diff --git a/.circleci/config.yml b/.circleci/config.yml index 6648b608d..cc41155be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -343,7 +343,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - yarn test:e2e:chrome + yarn test:e2e:chrome --retries 2 fi no_output_timeout: 20m - store_artifacts: @@ -370,7 +370,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - yarn test:e2e:chrome:metrics + yarn test:e2e:chrome:metrics --retries 2 fi no_output_timeout: 20m - store_artifacts: @@ -397,7 +397,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - yarn test:e2e:firefox + yarn test:e2e:firefox --retries 2 fi no_output_timeout: 20m - store_artifacts: @@ -424,7 +424,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - yarn test:e2e:firefox:metrics + yarn test:e2e:firefox:metrics --retries 2 fi no_output_timeout: 20m - store_artifacts: @@ -448,7 +448,7 @@ jobs: command: mv ./builds-test ./builds - run: name: Run page load benchmark - command: yarn benchmark:chrome --out test-artifacts/chrome/benchmark/pageload.json + command: yarn benchmark:chrome --out test-artifacts/chrome/benchmark/pageload.json --retries 2 - store_artifacts: path: test-artifacts destination: test-artifacts diff --git a/.circleci/scripts/chrome-install.sh b/.circleci/scripts/chrome-install.sh index c301f9056..37272e1a5 100755 --- a/.circleci/scripts/chrome-install.sh +++ b/.circleci/scripts/chrome-install.sh @@ -14,7 +14,10 @@ wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}" if [[ $(shasum -a 512 "${CHROME_BINARY}" | cut '--delimiter= ' -f1) != "${CHROME_BINARY_SHA512SUM}" ]] then + echo "Google Chrome binary checksum did not match." exit 1 +else + echo "Google Chrome binary checksum verified." fi (sudo dpkg -i "${CHROME_BINARY}" || sudo apt-get -fy install) diff --git a/.eslintrc.js b/.eslintrc.js index 56c2d4b86..d3763fdef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -154,7 +154,7 @@ module.exports = { 'babel.config.js', 'nyc.config.js', 'stylelint.config.js', - 'app/scripts/runLockdown.js', + 'app/scripts/lockdown-run.js', 'development/**/*.js', 'test/e2e/**/*.js', 'test/lib/wait-until-called.js', diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 18f384647..2d949ba94 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -15,9 +15,9 @@ const state = { "isAccountMenuOpen": false, "rpcUrl": "https://rawtestrpc.metamask.io/", "identities": { - "0x983211ce699ea5ab57cc528086154b6db1ad8e55": { - "name": "Account 1", - "address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55" + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { + "name": "This is a Really Long Account Name", + "address": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" }, "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": { "name": "Account 2", @@ -160,8 +160,8 @@ const state = { }, "network": "3", "accounts": { - "0x983211ce699ea5ab57cc528086154b6db1ad8e55": { - "address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55", + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { + "address": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", "balance": "0x176e5b6f173ebe66" }, "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": { @@ -200,7 +200,16 @@ const state = { "unapprovedPersonalMsgCount": 0, "unapprovedDecryptMsgs": {}, "unapprovedDecryptMsgCount": 0, - "unapprovedEncryptionPublicKeyMsgs": {}, + "unapprovedEncryptionPublicKeyMsgs": { + "7786962153682822": { + "id": 7786962153682822, + "msgParams": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "time": 1622687544054, + "status": "unapproved", + "type": "eth_getEncryptionPublicKey", + "origin": "https://metamask.github.io" + } + }, "unapprovedEncryptionPublicKeyMsgCount": 0, "unapprovedTypedMessages": {}, "unapprovedTypedMessagesCount": 0, @@ -260,7 +269,7 @@ const state = { } }, "assetImages": { - "0xad6d458402f60fd3bd25163575031acdce07538d": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xaD6D458402F60fD3Bd25163575031ACDce07538D/logo.png" + "0xad6d458402f60fd3bd25163575031acdce07538d": "./images/logo.png" }, "hiddenTokens": [], "suggestedTokens": {}, @@ -271,7 +280,7 @@ const state = { "ipfsGateway": "dweb.link", "infuraBlocked": false, "migratedPrivacyMode": false, - "selectedAddress": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "selectedAddress": "0x9d0ba4ddac06032527b140912ec808ab9451b788", "metaMetricsId": "0xc2377d11fec1c3b7dd88c4854240ee5e3ed0d9f63b00456d98d80320337b827f", "conversionDate": 1620710825.03, "conversionRate": 3910.28, diff --git a/CHANGELOG.md b/CHANGELOG.md index b5da4b270..cb5577db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#11031](https://github.com/MetaMask/metamask-extension/pull/11031): Fixes error behavior of addEthereumChain ## [9.5.9] +### Added +- Re-added "Add Ledger Live Support" ([#10293](https://github.com/MetaMask/metamask-extension/pull/10293)), which was reverted in the previous version + ### Fixed - [#11225](https://github.com/MetaMask/metamask-extension/pull/11225) - Fix persistent display of chrome ledger What's New popup message @@ -122,234 +125,235 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [9.5.3] ### Fixed -- [#11103](https://github.com/MetaMask/metamask-extension/pull/11103): Fixes bug that made MetaMask unusable and displayed 'Minified React error #130' on certain networks and accounts -- [#11015](https://github.com/MetaMask/metamask-extension/pull/11015): Prevent big number error when attempting to view transaction list +- Fixes bug that made MetaMask unusable and displayed 'Minified React error #130' on certain networks and accounts ([#11103](https://github.com/MetaMask/metamask-extension/pull/11103)) +- Prevent big number error when attempting to view transaction list ([#11015](https://github.com/MetaMask/metamask-extension/pull/11015)) ## [9.5.2] ### Fixed -- [#11071](https://github.com/MetaMask/metamask-extension/pull/11071): Fixing address entry error when sending a transaction on a custom network +- Fixing address entry error when sending a transaction on a custom network ([#11071](https://github.com/MetaMask/metamask-extension/pull/11071)) ## [9.5.1] ### Fixed -- [#11048](https://github.com/MetaMask/metamask-extension/pull/11048): Fixed icon on approval screen -- [#11036](https://github.com/MetaMask/metamask-extension/pull/11036): Fixed broken app state for some users with Chinese, Portugese or Spanish browser language settings. +- Fixed icon on approval screen ([#11048](https://github.com/MetaMask/metamask-extension/pull/11048)) +- Fixed broken app state for some users with Chinese, Portugese or Spanish browser language settings. ([#11036](https://github.com/MetaMask/metamask-extension/pull/11036)) ## [9.5.0] - 2021-04-28 ### Added -- [#10583](https://github.com/MetaMask/metamask-extension/pull/10583): Adding popup display to show new MetaMask notifications -- [#10938](https://github.com/MetaMask/metamask-extension/pull/10938): Add menu with "View on Etherscan" and "Account details" links to ETH asset page -- [#10932](https://github.com/MetaMask/metamask-extension/pull/10932): Add view account details menu item to token page menu -- [#10895](https://github.com/MetaMask/metamask-extension/pull/10895): Adding new links to contact MetaMask support -- [#10595](https://github.com/MetaMask/metamask-extension/pull/10595): Adding option to set Custom Nonce to Confirm Approve Page -- [#10616](https://github.com/MetaMask/metamask-extension/pull/10616): add trezor HD path for ledger wallets +- Adding popup display to show new MetaMask notifications ([#10583](https://github.com/MetaMask/metamask-extension/pull/10583)) +- Add menu with "View on Etherscan" and "Account details" links to ETH asset page ([#10938](https://github.com/MetaMask/metamask-extension/pull/10938)) +- Add view account details menu item to token page menu ([#10932](https://github.com/MetaMask/metamask-extension/pull/10932)) +- Adding new links to contact MetaMask support ([#10895](https://github.com/MetaMask/metamask-extension/pull/10895)) +- Adding option to set Custom Nonce to Confirm Approve Page ([#10595](https://github.com/MetaMask/metamask-extension/pull/10595)) +- Adding recovery phrase video to onboarding process ([#10717](https://github.com/MetaMask/metamask-extension/pull/10717)) +- add trezor HD path for ledger wallets ([#10616](https://github.com/MetaMask/metamask-extension/pull/10616)) ### Changed -- [#10939](https://github.com/MetaMask/metamask-extension/pull/10939): Use custom token icons in the send flow token dropdown -- [#10680](https://github.com/MetaMask/metamask-extension/pull/10680): Remove "My Wallet Account" section in Settings > Contact -- [#10912](https://github.com/MetaMask/metamask-extension/pull/10912): Harden contract address validation for token swaps -- [#10882](https://github.com/MetaMask/metamask-extension/pull/10882): Show the custom network name in swaps network fee tooltip -- [#10859](https://github.com/MetaMask/metamask-extension/pull/10859): Only check whether the swaps feature is live after entering the feature -- [#10871](https://github.com/MetaMask/metamask-extension/pull/10871): Update swaps metadata every 5 minutes as opposed to an hour -- [#10842](https://github.com/MetaMask/metamask-extension/pull/10842): Increase default slippage from 2% to 3% in swaps and show Advanced Options by default -- [#10593](https://github.com/MetaMask/metamask-extension/pull/10593): Prevent tokens without addresses from being added to token list -- [#10746](https://github.com/MetaMask/metamask-extension/pull/10746): Add New Zealand Dollar to currency options -- [#10670](https://github.com/MetaMask/metamask-extension/pull/10670): Allow 11 characters in symbol for custom RPCs -- [#10702](https://github.com/MetaMask/metamask-extension/pull/10702): Hide the suggested token pane when not on Mainnet or test network -- [#10700](https://github.com/MetaMask/metamask-extension/pull/10700): Prevents autocomplete text from displaying in the Add Token input -- [#10704](https://github.com/MetaMask/metamask-extension/pull/10704): Removing hard references to 12 word seed phrases in copy -- [#10703](https://github.com/MetaMask/metamask-extension/pull/10703): Add MetaMask to list of BIP44 HD path examples -- [#10651](https://github.com/MetaMask/metamask-extension/pull/10651): Change 'Send ETH' title to 'Send' in the send flow -- [#10674](https://github.com/MetaMask/metamask-extension/pull/10674): Don't render faucet row in deposit modal for custom chains +- Use custom token icons in the send flow token dropdown ([#10939](https://github.com/MetaMask/metamask-extension/pull/10939)) +- Remove "My Wallet Account" section in Settings > Contact ([#10680](https://github.com/MetaMask/metamask-extension/pull/10680)) +- Harden contract address validation for token swaps ([#10912](https://github.com/MetaMask/metamask-extension/pull/10912)) +- Show the custom network name in swaps network fee tooltip ([#10882](https://github.com/MetaMask/metamask-extension/pull/10882)) +- Only check whether the swaps feature is live after entering the feature ([#10859](https://github.com/MetaMask/metamask-extension/pull/10859)) +- Update swaps metadata every 5 minutes as opposed to an hour ([#10871](https://github.com/MetaMask/metamask-extension/pull/10871)) +- Increase default slippage from 2% to 3% in swaps and show Advanced Options by default ([#10842](https://github.com/MetaMask/metamask-extension/pull/10842)) +- Prevent tokens without addresses from being added to token list ([#10593](https://github.com/MetaMask/metamask-extension/pull/10593)) +- Add New Zealand Dollar to currency options ([#10746](https://github.com/MetaMask/metamask-extension/pull/10746)) +- Allow 11 characters in symbol for custom RPCs ([#10670](https://github.com/MetaMask/metamask-extension/pull/10670)) +- Hide the suggested token pane when not on Mainnet or test network ([#10702](https://github.com/MetaMask/metamask-extension/pull/10702)) +- Prevents autocomplete text from displaying in the Add Token input ([#10700](https://github.com/MetaMask/metamask-extension/pull/10700)) +- Removing hard references to 12 word seed phrases in copy ([#10704](https://github.com/MetaMask/metamask-extension/pull/10704)) +- Add MetaMask to list of BIP44 HD path examples ([#10703](https://github.com/MetaMask/metamask-extension/pull/10703)) +- Change 'Send ETH' title to 'Send' in the send flow ([#10651](https://github.com/MetaMask/metamask-extension/pull/10651)) +- Don't render faucet row in deposit modal for custom chains ([#10674](https://github.com/MetaMask/metamask-extension/pull/10674)) ### Fixed -- [#10935](https://github.com/MetaMask/metamask-extension/pull/10935): Prevent overflow of hostname on confirmation page -- [#10923](https://github.com/MetaMask/metamask-extension/pull/10923): Fixing ENS input entry in send flow -- [#10723](https://github.com/MetaMask/metamask-extension/pull/10723): Fix mismatchedChain typo in custom network approval screen -- [#10695](https://github.com/MetaMask/metamask-extension/pull/10695): Excluding sourcemaps comment in production builds -- [#10643](https://github.com/MetaMask/metamask-extension/pull/10643): Prevent network dropdown label highlighting -- [#10644](https://github.com/MetaMask/metamask-extension/pull/10644): Ensure swaps detail height doesn't create jump in vertical height -- [#10642](https://github.com/MetaMask/metamask-extension/pull/10642): Position the 3dot menu in the same spot on asset screen and home screen -- [#10594](https://github.com/MetaMask/metamask-extension/pull/10594): Ensure MetaMask works correctly when on a custom network that shares a chain id with a default Infura network -- [#10579](https://github.com/MetaMask/metamask-extension/pull/10579): Fixed bug that prevented speeding up cancelled transactions -- [#10630](https://github.com/MetaMask/metamask-extension/pull/10630): Fixes hidden token bug when zero balance preference is on -- [#10628](https://github.com/MetaMask/metamask-extension/pull/10628): Removing double click bug from delete custom network modal +- Prevent overflow of hostname on confirmation page ([#10935](https://github.com/MetaMask/metamask-extension/pull/10935)) +- Fixing ENS input entry in send flow ([#10923](https://github.com/MetaMask/metamask-extension/pull/10923)) +- Fix mismatchedChain typo in custom network approval screen ([#10723](https://github.com/MetaMask/metamask-extension/pull/10723)) +- Excluding sourcemaps comment in production builds ([#10695](https://github.com/MetaMask/metamask-extension/pull/10695)) +- Prevent network dropdown label highlighting ([#10643](https://github.com/MetaMask/metamask-extension/pull/10643)) +- Ensure swaps detail height doesn't create jump in vertical height ([#10644](https://github.com/MetaMask/metamask-extension/pull/10644)) +- Position the 3dot menu in the same spot on asset screen and home screen ([#10642](https://github.com/MetaMask/metamask-extension/pull/10642)) +- Ensure MetaMask works correctly when on a custom network that shares a chain id with a default Infura network ([#10594](https://github.com/MetaMask/metamask-extension/pull/10594)) +- Fixed bug that prevented speeding up cancelled transactions ([#10579](https://github.com/MetaMask/metamask-extension/pull/10579)) +- Fixes hidden token bug when zero balance preference is on ([#10630](https://github.com/MetaMask/metamask-extension/pull/10630)) +- Removing double click bug from delete custom network modal ([#10628](https://github.com/MetaMask/metamask-extension/pull/10628)) ## [9.4.0] - 2021-04-15 ### Added -- [#10883](https://github.com/MetaMask/metamask-extension/pull/10883): Notify users when MetaMask is unable to connect to the blockchain host +- Notify users when MetaMask is unable to connect to the blockchain host ([#10883](https://github.com/MetaMask/metamask-extension/pull/10883)) ## [9.3.0] - 2021-04-02 ### Added -- [#10721](https://github.com/MetaMask/metamask-extension/pull/10721): Swaps support for the Binance network -- [#10658](https://github.com/MetaMask/metamask-extension/pull/10658): Swaps support for forked Mainnet on localhost +- Swaps support for the Binance network ([#10721](https://github.com/MetaMask/metamask-extension/pull/10721)) +- Swaps support for forked Mainnet on localhost ([#10658](https://github.com/MetaMask/metamask-extension/pull/10658)) ### Fixed -- [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen -- [#10650](https://github.com/MetaMask/metamask-extension/pull/10650): Fix: ETH now only appears once in the swaps "to" and "from" dropdowns. +- Display BNB token image for default currency on BSC network home screen ([#10777](https://github.com/MetaMask/metamask-extension/pull/10777)) +- Fix: ETH now only appears once in the swaps "to" and "from" dropdowns. ([#10650](https://github.com/MetaMask/metamask-extension/pull/10650)) ## [9.2.1] - 2021-03-26 ### Fixed -- [#10692](https://github.com/MetaMask/metamask-extension/pull/10692): Prevent UI crash when a 'wallet_requestPermissions" confirmation is queued behind a "wallet_addEthereumChain" confirmation -- [#10712](https://github.com/MetaMask/metamask-extension/pull/10712): Fix infinite spinner when request for token symbol fails while attempting an approve transaction +- Prevent UI crash when a 'wallet_requestPermissions" confirmation is queued behind a "wallet_addEthereumChain" confirmation ([#10692](https://github.com/MetaMask/metamask-extension/pull/10692)) +- Fix infinite spinner when request for token symbol fails while attempting an approve transaction ([#10712](https://github.com/MetaMask/metamask-extension/pull/10712)) ## [9.2.0] - 2021-03-15 ### Added -- [#10546](https://github.com/MetaMask/metamask-extension/pull/10546): Add a warning when sending a token to its own contract address -- [#10582](https://github.com/MetaMask/metamask-extension/pull/10582): Adding warnings for excessive custom gas input -- [#10505](https://github.com/MetaMask/metamask-extension/pull/10505): Add support for multiple Ledger & Trezor hardware accounts -- [#10486](https://github.com/MetaMask/metamask-extension/pull/10486): Add setting to hide zero balance tokens +- Add a warning when sending a token to its own contract address ([#10546](https://github.com/MetaMask/metamask-extension/pull/10546)) +- Adding warnings for excessive custom gas input ([#10582](https://github.com/MetaMask/metamask-extension/pull/10582)) +- Add support for multiple Ledger & Trezor hardware accounts ([#10505](https://github.com/MetaMask/metamask-extension/pull/10505)) +- Add setting to hide zero balance tokens ([#10486](https://github.com/MetaMask/metamask-extension/pull/10486)) ### Changed -- [#10563](https://github.com/MetaMask/metamask-extension/pull/10563): Update references to MetaMask support -- [#10126](https://github.com/MetaMask/metamask-extension/pull/10126): Update Italian translation +- Update references to MetaMask support ([#10563](https://github.com/MetaMask/metamask-extension/pull/10563)) +- Update Italian translation ([#10126](https://github.com/MetaMask/metamask-extension/pull/10126)) ### Fixed -- [#10591](https://github.com/MetaMask/metamask-extension/pull/10591): Fix mobile sync of ERC20 tokens -- [#10601](https://github.com/MetaMask/metamask-extension/pull/10601): Fix activity title text truncation -- [#10598](https://github.com/MetaMask/metamask-extension/pull/10598): Remove 'Ethereum' from custom RPC endpoint warning -- [#10606](https://github.com/MetaMask/metamask-extension/pull/10606): Show loading screen while fetching token data for approve screen -- [#10587](https://github.com/MetaMask/metamask-extension/pull/10587): Show correct block explorer for custom RPC endpoints for built-in networks +- Fix mobile sync of ERC20 tokens ([#10591](https://github.com/MetaMask/metamask-extension/pull/10591)) +- Fix activity title text truncation ([#10601](https://github.com/MetaMask/metamask-extension/pull/10601)) +- Remove 'Ethereum' from custom RPC endpoint warning ([#10598](https://github.com/MetaMask/metamask-extension/pull/10598)) +- Show loading screen while fetching token data for approve screen ([#10606](https://github.com/MetaMask/metamask-extension/pull/10606)) +- Show correct block explorer for custom RPC endpoints for built-in networks ([#10587](https://github.com/MetaMask/metamask-extension/pull/10587)) ## [9.1.1] - 2021-03-03 ### Fixed -- [#10560](https://github.com/MetaMask/metamask-extension/pull/10560): Fix ENS resolution related crashes when switching networks on send screen -- [#10561](https://github.com/MetaMask/metamask-extension/pull/10561): Fix crash when speeding up an attempt to cancel a transaction on custom networks +- Fix ENS resolution related crashes when switching networks on send screen ([#10560](https://github.com/MetaMask/metamask-extension/pull/10560)) +- Fix crash when speeding up an attempt to cancel a transaction on custom networks ([#10561](https://github.com/MetaMask/metamask-extension/pull/10561)) ## [9.1.0] - 2021-02-01 ### Uncategorized -- [#10265](https://github.com/MetaMask/metamask-extension/pull/10265): Update Japanese translations. -- [#9388](https://github.com/MetaMask/metamask-extension/pull/9388): Update Chinese(Simplified) translations. -- [#10270](https://github.com/MetaMask/metamask-extension/pull/10270): Update Vietnamese translations. -- [#10258](https://github.com/MetaMask/metamask-extension/pull/10258): Update Spanish and Spanish(Latin American and Carribean) translations. -- [#10268](https://github.com/MetaMask/metamask-extension/pull/10268): Update Russian translations. -- [#10269](https://github.com/MetaMask/metamask-extension/pull/10269): Update Tagalog localized messages. -- [#10448](https://github.com/MetaMask/metamask-extension/pull/10448): Fix 'imported' translation use case for Dutch. -- [#10391](https://github.com/MetaMask/metamask-extension/pull/10391): Use translated transaction category for confirmations. -- [#10357](https://github.com/MetaMask/metamask-extension/pull/10357): Cancel unapproved confirmations on network change -- [#10413](https://github.com/MetaMask/metamask-extension/pull/10413): Use native currency in asset row. -- [#10421](https://github.com/MetaMask/metamask-extension/pull/10421): Fix color indicator size on connected site indicator. -- [#10423](https://github.com/MetaMask/metamask-extension/pull/10423): Fix multiple notification window prompts. -- [#10424](https://github.com/MetaMask/metamask-extension/pull/10424): Fix icons on token options menu. -- [#10414](https://github.com/MetaMask/metamask-extension/pull/10414): Fix token fiat conversion rates when switching from certain custom networks. -- [#10453](https://github.com/MetaMask/metamask-extension/pull/10453): Disable BUY button from home screen when not on Ethereum Mainnet. -- [#10465](https://github.com/MetaMask/metamask-extension/pull/10465): Fixes gas selection check mark on the notification view. -- [#10467](https://github.com/MetaMask/metamask-extension/pull/10467): Fix confirm page header with from/to addresses in fullscreen for tx confirmations. -- [#10455](https://github.com/MetaMask/metamask-extension/pull/10455): Hide links to etherscan when no block explorer is specified for a custom network for notifications. -- [#10456](https://github.com/MetaMask/metamask-extension/pull/10456): Fix swap insufficient balance error message. -- [#10350](https://github.com/MetaMask/metamask-extension/pull/10350): Fix encypt/decrypt tx queueing. -- [#10473](https://github.com/MetaMask/metamask-extension/pull/10473): Improve autofocus in the add network form. -- [#10444](https://github.com/MetaMask/metamask-extension/pull/10444): Use eth_gasprice for tx gas price estimation on non-Mainnet networks. -- [#10477](https://github.com/MetaMask/metamask-extension/pull/10477): Fix accountsChanged event not triggering when manually connecting. -- [#10471](https://github.com/MetaMask/metamask-extension/pull/10471): Fix navigation from jumping vertically when clicking into token. -- [#9724](https://github.com/MetaMask/metamask-extension/pull/9724): Add custom network RPC method. -- [#10496](https://github.com/MetaMask/metamask-extension/pull/10496): Eliminate artificial delay in swaps loading screen after request loading is complete. -- [#10501](https://github.com/MetaMask/metamask-extension/pull/10501): Ensure that swap approve tx and swap tx always have the same gas price. -- [#10485](https://github.com/MetaMask/metamask-extension/pull/10485): Fixes signTypedData message overflow. -- [#10525](https://github.com/MetaMask/metamask-extension/pull/10525): Update swaps failure message to include a support link. -- [#10521](https://github.com/MetaMask/metamask-extension/pull/10521): Accommodate for 0 sources verifying swap token -- [#10530](https://github.com/MetaMask/metamask-extension/pull/10530): Show warnings on Add Recipient page of Send flow -- [#9187](https://github.com/MetaMask/metamask-extension/pull/9187): Warn users when an ENS name contains 'confusable' characters -- [#10507](https://github.com/MetaMask/metamask-extension/pull/10507): Fixes ENS IPFS resolution on custom networks with the chainID of 1. +- Update Japanese translations. ([#10265](https://github.com/MetaMask/metamask-extension/pull/10265)) +- Update Chinese(Simplified) translations. ([#9388](https://github.com/MetaMask/metamask-extension/pull/9388)) +- Update Vietnamese translations. ([#10270](https://github.com/MetaMask/metamask-extension/pull/10270)) +- Update Spanish and Spanish(Latin American and Carribean) translations. ([#10258](https://github.com/MetaMask/metamask-extension/pull/10258)) +- Update Russian translations. ([#10268](https://github.com/MetaMask/metamask-extension/pull/10268)) +- Update Tagalog localized messages. ([#10269](https://github.com/MetaMask/metamask-extension/pull/10269)) +- Fix 'imported' translation use case for Dutch. ([#10448](https://github.com/MetaMask/metamask-extension/pull/10448)) +- Use translated transaction category for confirmations. ([#10391](https://github.com/MetaMask/metamask-extension/pull/10391)) +- Cancel unapproved confirmations on network change ([#10357](https://github.com/MetaMask/metamask-extension/pull/10357)) +- Use native currency in asset row. ([#10413](https://github.com/MetaMask/metamask-extension/pull/10413)) +- Fix color indicator size on connected site indicator. ([#10421](https://github.com/MetaMask/metamask-extension/pull/10421)) +- Fix multiple notification window prompts. ([#10423](https://github.com/MetaMask/metamask-extension/pull/10423)) +- Fix icons on token options menu. ([#10424](https://github.com/MetaMask/metamask-extension/pull/10424)) +- Fix token fiat conversion rates when switching from certain custom networks. ([#10414](https://github.com/MetaMask/metamask-extension/pull/10414)) +- Disable BUY button from home screen when not on Ethereum Mainnet. ([#10453](https://github.com/MetaMask/metamask-extension/pull/10453)) +- Fixes gas selection check mark on the notification view. ([#10465](https://github.com/MetaMask/metamask-extension/pull/10465)) +- Fix confirm page header with from/to addresses in fullscreen for tx confirmations. ([#10467](https://github.com/MetaMask/metamask-extension/pull/10467)) +- Hide links to etherscan when no block explorer is specified for a custom network for notifications. ([#10455](https://github.com/MetaMask/metamask-extension/pull/10455)) +- Fix swap insufficient balance error message. ([#10456](https://github.com/MetaMask/metamask-extension/pull/10456)) +- Fix encypt/decrypt tx queueing. ([#10350](https://github.com/MetaMask/metamask-extension/pull/10350)) +- Improve autofocus in the add network form. ([#10473](https://github.com/MetaMask/metamask-extension/pull/10473)) +- Use eth_gasprice for tx gas price estimation on non-Mainnet networks. ([#10444](https://github.com/MetaMask/metamask-extension/pull/10444)) +- Fix accountsChanged event not triggering when manually connecting. ([#10477](https://github.com/MetaMask/metamask-extension/pull/10477)) +- Fix navigation from jumping vertically when clicking into token. ([#10471](https://github.com/MetaMask/metamask-extension/pull/10471)) +- Add custom network RPC method. ([#9724](https://github.com/MetaMask/metamask-extension/pull/9724)) +- Eliminate artificial delay in swaps loading screen after request loading is complete. ([#10496](https://github.com/MetaMask/metamask-extension/pull/10496)) +- Ensure that swap approve tx and swap tx always have the same gas price. ([#10501](https://github.com/MetaMask/metamask-extension/pull/10501)) +- Fixes signTypedData message overflow. ([#10485](https://github.com/MetaMask/metamask-extension/pull/10485)) +- Update swaps failure message to include a support link. ([#10525](https://github.com/MetaMask/metamask-extension/pull/10525)) +- Accommodate for 0 sources verifying swap token ([#10521](https://github.com/MetaMask/metamask-extension/pull/10521)) +- Show warnings on Add Recipient page of Send flow ([#10530](https://github.com/MetaMask/metamask-extension/pull/10530)) +- Warn users when an ENS name contains 'confusable' characters ([#9187](https://github.com/MetaMask/metamask-extension/pull/9187)) +- Fixes ENS IPFS resolution on custom networks with the chainID of 1. ([#10507](https://github.com/MetaMask/metamask-extension/pull/10507)) ## [9.0.5] - 2021-02-09 ### Uncategorized -- [#10278](https://github.com/MetaMask/metamask-extension/pull/10278): Allow editing transaction amount after clicking max -- [#10214](https://github.com/MetaMask/metamask-extension/pull/10214): Standardize size, shape and color of network color indicators -- [#10298](https://github.com/MetaMask/metamask-extension/pull/10298): Use network primary currency instead of always defaulting to ETH in the confirm approve screen -- [#10300](https://github.com/MetaMask/metamask-extension/pull/10300): Add origin to signature request confirmation page -- [#10296](https://github.com/MetaMask/metamask-extension/pull/10296): Add origin to transaction confirmation -- [#10266](https://github.com/MetaMask/metamask-extension/pull/10266): Update `ko` localized messages -- [#10263](https://github.com/MetaMask/metamask-extension/pull/10263): Update `id` localized messages -- [#10347](https://github.com/MetaMask/metamask-extension/pull/10347): Require click of "Continue" button to interact with swap screen if there is a price impact warning for present swap -- [#10373](https://github.com/MetaMask/metamask-extension/pull/10373): Change copy of submit button on swaps screen -- [#10346](https://github.com/MetaMask/metamask-extension/pull/10346): Swaps token sources/verification messaging update -- [#10378](https://github.com/MetaMask/metamask-extension/pull/10378): Stop showing the window.web3 in-app popup if the dapp is just using web3.currentProvider -- [#10326](https://github.com/MetaMask/metamask-extension/pull/10326): Throw error when attempting to get an encryption key via eth_getEncryptionPublicKey when connected to Ledger HW -- [#10386](https://github.com/MetaMask/metamask-extension/pull/10386): Make action buttons on message components in swaps flow accessible +- Allow editing transaction amount after clicking max ([#10278](https://github.com/MetaMask/metamask-extension/pull/10278)) +- Standardize size, shape and color of network color indicators ([#10214](https://github.com/MetaMask/metamask-extension/pull/10214)) +- Use network primary currency instead of always defaulting to ETH in the confirm approve screen ([#10298](https://github.com/MetaMask/metamask-extension/pull/10298)) +- Add origin to signature request confirmation page ([#10300](https://github.com/MetaMask/metamask-extension/pull/10300)) +- Add origin to transaction confirmation ([#10296](https://github.com/MetaMask/metamask-extension/pull/10296)) +- Update `ko` localized messages ([#10266](https://github.com/MetaMask/metamask-extension/pull/10266)) +- Update `id` localized messages ([#10263](https://github.com/MetaMask/metamask-extension/pull/10263)) +- Require click of "Continue" button to interact with swap screen if there is a price impact warning for present swap ([#10347](https://github.com/MetaMask/metamask-extension/pull/10347)) +- Change copy of submit button on swaps screen ([#10373](https://github.com/MetaMask/metamask-extension/pull/10373)) +- Swaps token sources/verification messaging update ([#10346](https://github.com/MetaMask/metamask-extension/pull/10346)) +- Stop showing the window.web3 in-app popup if the dapp is just using web3.currentProvider ([#10378](https://github.com/MetaMask/metamask-extension/pull/10378)) +- Throw error when attempting to get an encryption key via eth_getEncryptionPublicKey when connected to Ledger HW ([#10326](https://github.com/MetaMask/metamask-extension/pull/10326)) +- Make action buttons on message components in swaps flow accessible ([#10386](https://github.com/MetaMask/metamask-extension/pull/10386)) ## [9.0.4] - 2021-01-27 ### Uncategorized -- [#10285](https://github.com/MetaMask/metamask-extension/pull/10285): Update @metamask/contract-metadata from v1.21.0 to 1.22.0 -- [#10264](https://github.com/MetaMask/metamask-extension/pull/10264): Update `hi` localized messages -- [#10174](https://github.com/MetaMask/metamask-extension/pull/10174): Move fox to bottom of 'About' page -- [#10198](https://github.com/MetaMask/metamask-extension/pull/10198): Fix hardware account selection -- [#10101](https://github.com/MetaMask/metamask-extension/pull/10101): Add a timeout to all network requests -- [#10212](https://github.com/MetaMask/metamask-extension/pull/10212): Fix displayed balance of tokens with 0 decimals in swaps flow -- [#10162](https://github.com/MetaMask/metamask-extension/pull/10162): Prevent accidentally submitting a swap twice -- [#10224](https://github.com/MetaMask/metamask-extension/pull/10224): Improve chain ID validation -- [#10195](https://github.com/MetaMask/metamask-extension/pull/10195): Increase minimum Firefox version to v68 -- [#10192](https://github.com/MetaMask/metamask-extension/pull/10192): Update TrezorConnect to v8 -- [#10166](https://github.com/MetaMask/metamask-extension/pull/10166): Fix back button on swaps loading page -- [#9947](https://github.com/MetaMask/metamask-extension/pull/9947): Do not publish swaps transaction if the estimateGas call made when adding the transaction fails. +- Update @metamask/contract-metadata from v1.21.0 to 1.22.0 ([#10285](https://github.com/MetaMask/metamask-extension/pull/10285)) +- Update `hi` localized messages ([#10264](https://github.com/MetaMask/metamask-extension/pull/10264)) +- Move fox to bottom of 'About' page ([#10174](https://github.com/MetaMask/metamask-extension/pull/10174)) +- Fix hardware account selection ([#10198](https://github.com/MetaMask/metamask-extension/pull/10198)) +- Add a timeout to all network requests ([#10101](https://github.com/MetaMask/metamask-extension/pull/10101)) +- Fix displayed balance of tokens with 0 decimals in swaps flow ([#10212](https://github.com/MetaMask/metamask-extension/pull/10212)) +- Prevent accidentally submitting a swap twice ([#10162](https://github.com/MetaMask/metamask-extension/pull/10162)) +- Improve chain ID validation ([#10224](https://github.com/MetaMask/metamask-extension/pull/10224)) +- Increase minimum Firefox version to v68 ([#10195](https://github.com/MetaMask/metamask-extension/pull/10195)) +- Update TrezorConnect to v8 ([#10192](https://github.com/MetaMask/metamask-extension/pull/10192)) +- Fix back button on swaps loading page ([#10166](https://github.com/MetaMask/metamask-extension/pull/10166)) +- Do not publish swaps transaction if the estimateGas call made when adding the transaction fails. ([#9947](https://github.com/MetaMask/metamask-extension/pull/9947)) ## [9.0.3] - 2021-01-22 ### Uncategorized -- [#10243](https://github.com/MetaMask/metamask-extension/pull/10243): Fix site metadata handling -- [#10252](https://github.com/MetaMask/metamask-extension/pull/10252): Fix decrypt message confirmation UI crash +- Fix site metadata handling ([#10243](https://github.com/MetaMask/metamask-extension/pull/10243)) +- Fix decrypt message confirmation UI crash ([#10252](https://github.com/MetaMask/metamask-extension/pull/10252)) ## [9.0.2] - 2021-01-20 ### Uncategorized -- [#10191](https://github.com/MetaMask/metamask-extension/pull/10191): zh_TW: 乙太 -> 以太 (#10191) -- [#10207](https://github.com/MetaMask/metamask-extension/pull/10207): zh_TW: Translate buy, assets, activity (#10207) -- [#10219](https://github.com/MetaMask/metamask-extension/pull/10219): Restore provider 'data' event (#10219) +- zh_TW: 乙太 -> 以太 ([#10191](https://github.com/MetaMask/metamask-extension/pull/10191)) +- zh_TW: Translate buy, assets, activity ([#10207](https://github.com/MetaMask/metamask-extension/pull/10207)) +- Restore provider 'data' event ([#10219](https://github.com/MetaMask/metamask-extension/pull/10219)) ## [9.0.1] - 2021-01-13 ### Uncategorized -- [#10169](https://github.com/MetaMask/metamask-extension/pull/10169): Improved detection of contract methods with array parameters -- [#10178](https://github.com/MetaMask/metamask-extension/pull/10178): Only warn of injected web3 usage once per page -- [#10179](https://github.com/MetaMask/metamask-extension/pull/10179): Restore support for @metamask/inpage provider@"< 8.0.0" -- [#10180](https://github.com/MetaMask/metamask-extension/pull/10180): Fix UI crash when domain metadata is missing on public encryption key confirmation page +- Improved detection of contract methods with array parameters ([#10169](https://github.com/MetaMask/metamask-extension/pull/10169)) +- Only warn of injected web3 usage once per page ([#10178](https://github.com/MetaMask/metamask-extension/pull/10178)) +- Restore support for @metamask/inpage provider@"< 8.0.0" ([#10179](https://github.com/MetaMask/metamask-extension/pull/10179)) +- Fix UI crash when domain metadata is missing on public encryption key confirmation page ([#10180](https://github.com/MetaMask/metamask-extension/pull/10180)) ## [9.0.0] - 2021-01-12 ### Uncategorized -- [#9156](https://github.com/MetaMask/metamask-extension/pull/9156): Remove window.web3 injection -- [#10039](https://github.com/MetaMask/metamask-extension/pull/10039): Add web3 shim usage notification -- [#8640](https://github.com/MetaMask/metamask-extension/pull/8640): Implement breaking window.ethereum API changes -- [#8629](https://github.com/MetaMask/metamask-extension/pull/8629): Fix `eth_chainId` return values for Infura networks -- [#10019](https://github.com/MetaMask/metamask-extension/pull/10019): Increase Chrome minimum version to v63 -- [#10135](https://github.com/MetaMask/metamask-extension/pull/10135): Fix error where a swap only completed the token approval transaction -- [#10100](https://github.com/MetaMask/metamask-extension/pull/10100): Remove unnecessary swaps footer space when in dropdown mode -- [#9905](https://github.com/MetaMask/metamask-extension/pull/9905): Redesign view quote screens -- [#9320](https://github.com/MetaMask/metamask-extension/pull/9320): Prevent hidden tokens from reappearing -- [#10000](https://github.com/MetaMask/metamask-extension/pull/10000): Use consistent font size for modal top right Close links -- [#10046](https://github.com/MetaMask/metamask-extension/pull/10046): Improve home screen notification appearance -- [#10093](https://github.com/MetaMask/metamask-extension/pull/10093): Always roll back to the previously selected network when unable to connect to a newly selected network -- [#10117](https://github.com/MetaMask/metamask-extension/pull/10117): Fix network settings Kovan block explorer link -- [#10143](https://github.com/MetaMask/metamask-extension/pull/10143): Prevent malformed next nonce warning -- [#10142](https://github.com/MetaMask/metamask-extension/pull/10142): Update @metamask/contract-metadata from v1.20.0 to 1.21.0 -- [#10160](https://github.com/MetaMask/metamask-extension/pull/10160): Fix French "Block Explorer URL" translations -- [#10157](https://github.com/MetaMask/metamask-extension/pull/10157): Automatically detect tokens on custom Mainnet RPC endpoints -- [#9772](https://github.com/MetaMask/metamask-extension/pull/9772): Improve zh_CN translation -- [#10170](https://github.com/MetaMask/metamask-extension/pull/10170): Fix bug where swaps button was disabled on Mainnet if the user hadn't switched networks in a long time +- Remove window.web3 injection ([#9156](https://github.com/MetaMask/metamask-extension/pull/9156)) +- Add web3 shim usage notification ([#10039](https://github.com/MetaMask/metamask-extension/pull/10039)) +- Implement breaking window.ethereum API changes ([#8640](https://github.com/MetaMask/metamask-extension/pull/8640)) +- Fix `eth_chainId` return values for Infura networks ([#8629](https://github.com/MetaMask/metamask-extension/pull/8629)) +- Increase Chrome minimum version to v63 ([#10019](https://github.com/MetaMask/metamask-extension/pull/10019)) +- Fix error where a swap only completed the token approval transaction ([#10135](https://github.com/MetaMask/metamask-extension/pull/10135)) +- Remove unnecessary swaps footer space when in dropdown mode ([#10100](https://github.com/MetaMask/metamask-extension/pull/10100)) +- Redesign view quote screens ([#9905](https://github.com/MetaMask/metamask-extension/pull/9905)) +- Prevent hidden tokens from reappearing ([#9320](https://github.com/MetaMask/metamask-extension/pull/9320)) +- Use consistent font size for modal top right Close links ([#10000](https://github.com/MetaMask/metamask-extension/pull/10000)) +- Improve home screen notification appearance ([#10046](https://github.com/MetaMask/metamask-extension/pull/10046)) +- Always roll back to the previously selected network when unable to connect to a newly selected network ([#10093](https://github.com/MetaMask/metamask-extension/pull/10093)) +- Fix network settings Kovan block explorer link ([#10117](https://github.com/MetaMask/metamask-extension/pull/10117)) +- Prevent malformed next nonce warning ([#10143](https://github.com/MetaMask/metamask-extension/pull/10143)) +- Update @metamask/contract-metadata from v1.20.0 to 1.21.0 ([#10142](https://github.com/MetaMask/metamask-extension/pull/10142)) +- Fix French "Block Explorer URL" translations ([#10160](https://github.com/MetaMask/metamask-extension/pull/10160)) +- Automatically detect tokens on custom Mainnet RPC endpoints ([#10157](https://github.com/MetaMask/metamask-extension/pull/10157)) +- Improve zh_CN translation ([#9772](https://github.com/MetaMask/metamask-extension/pull/9772)) +- Fix bug where swaps button was disabled on Mainnet if the user hadn't switched networks in a long time ([#10170](https://github.com/MetaMask/metamask-extension/pull/10170)) ## [8.1.11] - 2021-01-07 ### Uncategorized -- [#10155](https://github.com/MetaMask/metamask-extension/pull/10155): Disable swaps when the current network's chainId does not match the mainnet chain ID, instead of disabling based on network ID +- Disable swaps when the current network's chainId does not match the mainnet chain ID, instead of disabling based on network ID ([#10155](https://github.com/MetaMask/metamask-extension/pull/10155)) ## [8.1.10] - 2021-01-04 ### Uncategorized -- [#10084](https://github.com/MetaMask/metamask-extension/pull/10084): Set last provider when switching to a customRPC -- [#10096](https://github.com/MetaMask/metamask-extension/pull/10096): Update `@metamask/controllers` to v5.1.0 -- [#10103](https://github.com/MetaMask/metamask-extension/pull/10103): Prevent stuck loading screen in some situations -- [#10104](https://github.com/MetaMask/metamask-extension/pull/10104): Bump @metamask/contract-metadata from 1.19.0 to 1.20.0 -- [#10110](https://github.com/MetaMask/metamask-extension/pull/10110): Fix frozen loading screen on Firefox when strict Enhanced Tracking Protection is enabled +- Set last provider when switching to a customRPC ([#10084](https://github.com/MetaMask/metamask-extension/pull/10084)) +- Update `@metamask/controllers` to v5.1.0 ([#10096](https://github.com/MetaMask/metamask-extension/pull/10096)) +- Prevent stuck loading screen in some situations ([#10103](https://github.com/MetaMask/metamask-extension/pull/10103)) +- Bump @metamask/contract-metadata from 1.19.0 to 1.20.0 ([#10104](https://github.com/MetaMask/metamask-extension/pull/10104)) +- Fix frozen loading screen on Firefox when strict Enhanced Tracking Protection is enabled ([#10110](https://github.com/MetaMask/metamask-extension/pull/10110)) ## [8.1.9] - 2020-12-15 ### Uncategorized -- [#10034](https://github.com/MetaMask/metamask-extension/pull/10034): Fix contentscript injection failure on Firefox 56 -- [#10045](https://github.com/MetaMask/metamask-extension/pull/10045): Fix token validation in Send flow -- [#10048](https://github.com/MetaMask/metamask-extension/pull/10048): Display boolean values when signing typed data -- [#10070](https://github.com/MetaMask/metamask-extension/pull/10070): Add eth_getProof -- [#10043](https://github.com/MetaMask/metamask-extension/pull/10043): Improve swaps maximum gas estimation -- [#10069](https://github.com/MetaMask/metamask-extension/pull/10069): Fetch swap quote refresh time from API -- [#10040](https://github.com/MetaMask/metamask-extension/pull/10040): Disable console in contentscript to reduce noise +- Fix contentscript injection failure on Firefox 56 ([#10034](https://github.com/MetaMask/metamask-extension/pull/10034)) +- Fix token validation in Send flow ([#10045](https://github.com/MetaMask/metamask-extension/pull/10045)) +- Display boolean values when signing typed data ([#10048](https://github.com/MetaMask/metamask-extension/pull/10048)) +- Add eth_getProof ([#10070](https://github.com/MetaMask/metamask-extension/pull/10070)) +- Improve swaps maximum gas estimation ([#10043](https://github.com/MetaMask/metamask-extension/pull/10043)) +- Fetch swap quote refresh time from API ([#10069](https://github.com/MetaMask/metamask-extension/pull/10069)) +- Disable console in contentscript to reduce noise ([#10040](https://github.com/MetaMask/metamask-extension/pull/10040)) ## [8.1.8] - 2020-12-09 ### Uncategorized -- [#9992](https://github.com/MetaMask/metamask-extension/pull/9992): Improve transaction params validation -- [#9991](https://github.com/MetaMask/metamask-extension/pull/9991): Don't allow more than 15% slippage -- [#9994](https://github.com/MetaMask/metamask-extension/pull/9994): Prevent unwanted 'no quotes available' message when going back to build quote screen while having insufficient funds -- [#9999](https://github.com/MetaMask/metamask-extension/pull/9999): Fix missing contacts upon restart +- Improve transaction params validation ([#9992](https://github.com/MetaMask/metamask-extension/pull/9992)) +- Don't allow more than 15% slippage ([#9991](https://github.com/MetaMask/metamask-extension/pull/9991)) +- Prevent unwanted 'no quotes available' message when going back to build quote screen while having insufficient funds ([#9994](https://github.com/MetaMask/metamask-extension/pull/9994)) +- Fix missing contacts upon restart ([#9999](https://github.com/MetaMask/metamask-extension/pull/9999)) ## [8.1.7] - 2020-12-09 ### Uncategorized @@ -357,840 +361,841 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [8.1.6] - 2020-12-04 ### Uncategorized -- [#9916](https://github.com/MetaMask/metamask-extension/pull/9916): Fix QR code scans interpretting payment requests as token addresses -- [#9847](https://github.com/MetaMask/metamask-extension/pull/9847): Add alt text for images in list items -- [#9960](https://github.com/MetaMask/metamask-extension/pull/9960): Ensure watchAsset returns errors for invalid token symbols -- [#9968](https://github.com/MetaMask/metamask-extension/pull/9968): Adds tokens from v1.19.0 of metamask/contract-metadata to add token lists -- [#9970](https://github.com/MetaMask/metamask-extension/pull/9970): Etherscan links support Goerli network -- [#9899](https://github.com/MetaMask/metamask-extension/pull/9899): Show price impact warnings on swaps quote screen -- [#9867](https://github.com/MetaMask/metamask-extension/pull/9867): Replace use of ethgasstation -- [#9984](https://github.com/MetaMask/metamask-extension/pull/9984): Show correct gas estimates when users don't have sufficient balance for contract transaction -- [#9993](https://github.com/MetaMask/metamask-extension/pull/9993): Add 48x48 MetaMask icon for use by browsers +- Fix QR code scans interpretting payment requests as token addresses ([#9916](https://github.com/MetaMask/metamask-extension/pull/9916)) +- Add alt text for images in list items ([#9847](https://github.com/MetaMask/metamask-extension/pull/9847)) +- Ensure watchAsset returns errors for invalid token symbols ([#9960](https://github.com/MetaMask/metamask-extension/pull/9960)) +- Adds tokens from v1.19.0 of metamask/contract-metadata to add token lists ([#9968](https://github.com/MetaMask/metamask-extension/pull/9968)) +- Etherscan links support Goerli network ([#9970](https://github.com/MetaMask/metamask-extension/pull/9970)) +- Show price impact warnings on swaps quote screen ([#9899](https://github.com/MetaMask/metamask-extension/pull/9899)) +- Replace use of ethgasstation ([#9867](https://github.com/MetaMask/metamask-extension/pull/9867)) +- Show correct gas estimates when users don't have sufficient balance for contract transaction ([#9984](https://github.com/MetaMask/metamask-extension/pull/9984)) +- Add 48x48 MetaMask icon for use by browsers ([#9993](https://github.com/MetaMask/metamask-extension/pull/9993)) ## [8.1.5] - 2020-11-19 ### Uncategorized -- [#9871](https://github.com/MetaMask/metamask-extension/pull/9871): Show send text upon hover in main asset list -- [#9855](https://github.com/MetaMask/metamask-extension/pull/9855): Make edit icon and account name in account details modal focusable -- [#9853](https://github.com/MetaMask/metamask-extension/pull/9853): Provide alternative text for images where appropriate -- [#9869](https://github.com/MetaMask/metamask-extension/pull/9869): Remove CoinSwitch from the Deposit modal -- [#9883](https://github.com/MetaMask/metamask-extension/pull/9883): Move add contact button in fullscreen/expanded view of settings lower to expose the close button. -- [#9891](https://github.com/MetaMask/metamask-extension/pull/9891): Add token verification message to swaps build quote screen -- [#9896](https://github.com/MetaMask/metamask-extension/pull/9896): Show failed token balance updates -- [#9909](https://github.com/MetaMask/metamask-extension/pull/9909): Update asset page etherscan link to the address-filtered token page on Etherscan -- [#9910](https://github.com/MetaMask/metamask-extension/pull/9910): Revert "Show a 'send eth' button on home screen in full screen mode" -- [#9907](https://github.com/MetaMask/metamask-extension/pull/9907): Ensure "Known contract address" warning is shown on send screen even when changing asset -- [#9911](https://github.com/MetaMask/metamask-extension/pull/9911): Fix display of Ledger connection error -- [#9918](https://github.com/MetaMask/metamask-extension/pull/9918): Fix missing icon in asset page dropdown and in advanced gas modal button group +- Show send text upon hover in main asset list ([#9871](https://github.com/MetaMask/metamask-extension/pull/9871)) +- Make edit icon and account name in account details modal focusable ([#9855](https://github.com/MetaMask/metamask-extension/pull/9855)) +- Provide alternative text for images where appropriate ([#9853](https://github.com/MetaMask/metamask-extension/pull/9853)) +- Remove CoinSwitch from the Deposit modal ([#9869](https://github.com/MetaMask/metamask-extension/pull/9869)) +- Move add contact button in fullscreen/expanded view of settings lower to expose the close button. ([#9883](https://github.com/MetaMask/metamask-extension/pull/9883)) +- Add token verification message to swaps build quote screen ([#9891](https://github.com/MetaMask/metamask-extension/pull/9891)) +- Show failed token balance updates ([#9896](https://github.com/MetaMask/metamask-extension/pull/9896)) +- Update asset page etherscan link to the address-filtered token page on Etherscan ([#9909](https://github.com/MetaMask/metamask-extension/pull/9909)) +- Revert "Show a 'send eth' button on home screen in full screen mode" ([#9910](https://github.com/MetaMask/metamask-extension/pull/9910)) +- Ensure "Known contract address" warning is shown on send screen even when changing asset ([#9907](https://github.com/MetaMask/metamask-extension/pull/9907)) +- Fix display of Ledger connection error ([#9911](https://github.com/MetaMask/metamask-extension/pull/9911)) +- Fix missing icon in asset page dropdown and in advanced gas modal button group ([#9918](https://github.com/MetaMask/metamask-extension/pull/9918)) ## [8.1.4] - 2020-11-16 ### Uncategorized -- [#9687](https://github.com/MetaMask/metamask-extension/pull/9687): Allow speeding up of underpriced transactions -- [#9694](https://github.com/MetaMask/metamask-extension/pull/9694): normalize UI component font styles -- [#9695](https://github.com/MetaMask/metamask-extension/pull/9695): normalize app component font styles -- [#9696](https://github.com/MetaMask/metamask-extension/pull/9696): normalize deprecated itcss font styles -- [#9697](https://github.com/MetaMask/metamask-extension/pull/9697): normalize page font styles -- [#9740](https://github.com/MetaMask/metamask-extension/pull/9740): Standardize network settings page -- [#9750](https://github.com/MetaMask/metamask-extension/pull/9750): Make swap arrows accessible, make swaps advanced options accessible -- [#9766](https://github.com/MetaMask/metamask-extension/pull/9766): Use 1px borders on inputs and buttons -- [#9767](https://github.com/MetaMask/metamask-extension/pull/9767): Remove border radius from transfer button -- [#9764](https://github.com/MetaMask/metamask-extension/pull/9764): Update custom RPC network dropdown icons -- [#9763](https://github.com/MetaMask/metamask-extension/pull/9763): Add confirmation for network dropdown delete action -- [#9583](https://github.com/MetaMask/metamask-extension/pull/9583): Use `chainId` for incoming transactions controller -- [#9748](https://github.com/MetaMask/metamask-extension/pull/9748): Autofocus input, improve accessibility of restore page -- [#9778](https://github.com/MetaMask/metamask-extension/pull/9778): Shorten unit input width and use ellipses for overflow -- [#9746](https://github.com/MetaMask/metamask-extension/pull/9746): Make the login screen's Restore and Import links accessible -- [#9780](https://github.com/MetaMask/metamask-extension/pull/9780): Display decimal chain ID in network form -- [#9599](https://github.com/MetaMask/metamask-extension/pull/9599): Use MetaSwap API for gas price estimation in swaps -- [#9518](https://github.com/MetaMask/metamask-extension/pull/9518): Make all UI tabs accessible via keyboard -- [#9808](https://github.com/MetaMask/metamask-extension/pull/9808): Always allow overwriting invalid custom RPC chain ID -- [#9812](https://github.com/MetaMask/metamask-extension/pull/9812): Fix send header cancel button alignment -- [#9271](https://github.com/MetaMask/metamask-extension/pull/9271): Do not check popupIsOpen on Vivaldi -- [#9306](https://github.com/MetaMask/metamask-extension/pull/9306): Fix UI crash when dapp submits negative gas price -- [#9257](https://github.com/MetaMask/metamask-extension/pull/9257): Add sort and search to AddRecipient accounts list -- [#9824](https://github.com/MetaMask/metamask-extension/pull/9824): Move `externally_connectable` from base to Chrome manifest -- [#9815](https://github.com/MetaMask/metamask-extension/pull/9815): Add support for custom network RPC URL with basic auth -- [#9822](https://github.com/MetaMask/metamask-extension/pull/9822): Make QR code button focusable -- [#9832](https://github.com/MetaMask/metamask-extension/pull/9832): Warn instead of throw on duplicate web3 -- [#9838](https://github.com/MetaMask/metamask-extension/pull/9838): @metamask/controllers@4.0.0 -- [#9856](https://github.com/MetaMask/metamask-extension/pull/9856): Prevent user from getting stuck on opt in page -- [#9845](https://github.com/MetaMask/metamask-extension/pull/9845): Show a 'send eth' button on home screen in full screen mode -- [#9871](https://github.com/MetaMask/metamask-extension/pull/9871): Show send text upon hover in main asset list -- [#9880](https://github.com/MetaMask/metamask-extension/pull/9880): Properly detect U2F errors in hardware wallet +- Allow speeding up of underpriced transactions ([#9687](https://github.com/MetaMask/metamask-extension/pull/9687)) +- normalize UI component font styles ([#9694](https://github.com/MetaMask/metamask-extension/pull/9694)) +- normalize app component font styles ([#9695](https://github.com/MetaMask/metamask-extension/pull/9695)) +- normalize deprecated itcss font styles ([#9696](https://github.com/MetaMask/metamask-extension/pull/9696)) +- normalize page font styles ([#9697](https://github.com/MetaMask/metamask-extension/pull/9697)) +- Standardize network settings page ([#9740](https://github.com/MetaMask/metamask-extension/pull/9740)) +- Make swap arrows accessible, make swaps advanced options accessible ([#9750](https://github.com/MetaMask/metamask-extension/pull/9750)) +- Use 1px borders on inputs and buttons ([#9766](https://github.com/MetaMask/metamask-extension/pull/9766)) +- Remove border radius from transfer button ([#9767](https://github.com/MetaMask/metamask-extension/pull/9767)) +- Update custom RPC network dropdown icons ([#9764](https://github.com/MetaMask/metamask-extension/pull/9764)) +- Add confirmation for network dropdown delete action ([#9763](https://github.com/MetaMask/metamask-extension/pull/9763)) +- Use `chainId` for incoming transactions controller ([#9583](https://github.com/MetaMask/metamask-extension/pull/9583)) +- Autofocus input, improve accessibility of restore page ([#9748](https://github.com/MetaMask/metamask-extension/pull/9748)) +- Shorten unit input width and use ellipses for overflow ([#9778](https://github.com/MetaMask/metamask-extension/pull/9778)) +- Make the login screen's Restore and Import links accessible ([#9746](https://github.com/MetaMask/metamask-extension/pull/9746)) +- Display decimal chain ID in network form ([#9780](https://github.com/MetaMask/metamask-extension/pull/9780)) +- Use MetaSwap API for gas price estimation in swaps ([#9599](https://github.com/MetaMask/metamask-extension/pull/9599)) +- Make all UI tabs accessible via keyboard ([#9518](https://github.com/MetaMask/metamask-extension/pull/9518)) +- Always allow overwriting invalid custom RPC chain ID ([#9808](https://github.com/MetaMask/metamask-extension/pull/9808)) +- Fix send header cancel button alignment ([#9812](https://github.com/MetaMask/metamask-extension/pull/9812)) +- Do not check popupIsOpen on Vivaldi ([#9271](https://github.com/MetaMask/metamask-extension/pull/9271)) +- Fix UI crash when dapp submits negative gas price ([#9306](https://github.com/MetaMask/metamask-extension/pull/9306)) +- Add sort and search to AddRecipient accounts list ([#9257](https://github.com/MetaMask/metamask-extension/pull/9257)) +- Move `externally_connectable` from base to Chrome manifest ([#9824](https://github.com/MetaMask/metamask-extension/pull/9824)) +- Add support for custom network RPC URL with basic auth ([#9815](https://github.com/MetaMask/metamask-extension/pull/9815)) +- Make QR code button focusable ([#9822](https://github.com/MetaMask/metamask-extension/pull/9822)) +- Warn instead of throw on duplicate web3 ([#9832](https://github.com/MetaMask/metamask-extension/pull/9832)) +- @metamask/controllers@4.0.0 ([#9838](https://github.com/MetaMask/metamask-extension/pull/9838)) +- Prevent user from getting stuck on opt in page ([#9856](https://github.com/MetaMask/metamask-extension/pull/9856)) +- Show a 'send eth' button on home screen in full screen mode ([#9845](https://github.com/MetaMask/metamask-extension/pull/9845)) +- Show send text upon hover in main asset list ([#9871](https://github.com/MetaMask/metamask-extension/pull/9871)) +- Properly detect U2F errors in hardware wallet ([#9880](https://github.com/MetaMask/metamask-extension/pull/9880)) ## [8.1.3] - 2020-10-29 ### Uncategorized -- [#9642](https://github.com/MetaMask/metamask-extension/pull/9642) Prevent excessive overflow from swap dropdowns -- [#9658](https://github.com/MetaMask/metamask-extension/pull/9658): Fix sorting Quote Source column of quote sort list -- [#9667](https://github.com/MetaMask/metamask-extension/pull/9667): Fix adding contact with QR code -- [#9674](https://github.com/MetaMask/metamask-extension/pull/9674): Fix ENS resolution of `.eth` URLs with query strings -- [#9691](https://github.com/MetaMask/metamask-extension/pull/9691): Bump @metamask/inpage-provider from 6.1.0 to 6.3.0 -- [#9700](https://github.com/MetaMask/metamask-extension/pull/9700): Provide image sizing so there's no jump when opening the swaps token search -- [#9568](https://github.com/MetaMask/metamask-extension/pull/9568): Add ses lockdown to build system -- [#9705](https://github.com/MetaMask/metamask-extension/pull/9705): Prevent memory leak from selected account copy tooltip -- [#9671](https://github.com/MetaMask/metamask-extension/pull/9671): Prevent old fetches from polluting the swap state -- [#9702](https://github.com/MetaMask/metamask-extension/pull/9702): Keyboard navigation for swaps dropdowns -- [#9646](https://github.com/MetaMask/metamask-extension/pull/9646): Switch from Matomo to Segment -- [#9745](https://github.com/MetaMask/metamask-extension/pull/9745): Fix fetching swaps when initial network not Mainnet -- [#9621](https://github.com/MetaMask/metamask-extension/pull/9621): Include aggregator fee as part of displayed network fees -- [#9736](https://github.com/MetaMask/metamask-extension/pull/9736): Bump eth-contract-metadata from 1.16.0 to 1.17.0 -- [#9743](https://github.com/MetaMask/metamask-extension/pull/9743): Fix "+-" prefix on swap token amount -- [#9715](https://github.com/MetaMask/metamask-extension/pull/9715): Focus on wallet address in buy workflow +- Prevent excessive overflow from swap dropdowns ([#9642](https://github.com/MetaMask/metamask-extension/pull/9642)) +- Fix sorting Quote Source column of quote sort list ([#9658](https://github.com/MetaMask/metamask-extension/pull/9658)) +- Fix adding contact with QR code ([#9667](https://github.com/MetaMask/metamask-extension/pull/9667)) +- Fix ENS resolution of `.eth` URLs with query strings ([#9674](https://github.com/MetaMask/metamask-extension/pull/9674)) +- Bump @metamask/inpage-provider from 6.1.0 to 6.3.0 ([#9691](https://github.com/MetaMask/metamask-extension/pull/9691)) +- Provide image sizing so there's no jump when opening the swaps token search ([#9700](https://github.com/MetaMask/metamask-extension/pull/9700)) +- Add ses lockdown to build system ([#9568](https://github.com/MetaMask/metamask-extension/pull/9568)) +- Prevent memory leak from selected account copy tooltip ([#9705](https://github.com/MetaMask/metamask-extension/pull/9705)) +- Prevent old fetches from polluting the swap state ([#9671](https://github.com/MetaMask/metamask-extension/pull/9671)) +- Keyboard navigation for swaps dropdowns ([#9702](https://github.com/MetaMask/metamask-extension/pull/9702)) +- Switch from Matomo to Segment ([#9646](https://github.com/MetaMask/metamask-extension/pull/9646)) +- Fix fetching swaps when initial network not Mainnet ([#9745](https://github.com/MetaMask/metamask-extension/pull/9745)) +- Include aggregator fee as part of displayed network fees ([#9621](https://github.com/MetaMask/metamask-extension/pull/9621)) +- Bump eth-contract-metadata from 1.16.0 to 1.17.0 ([#9736](https://github.com/MetaMask/metamask-extension/pull/9736)) +- Fix "+-" prefix on swap token amount ([#9743](https://github.com/MetaMask/metamask-extension/pull/9743)) +- Focus on wallet address in buy workflow ([#9715](https://github.com/MetaMask/metamask-extension/pull/9715)) ## [8.1.2] - 2020-10-20 ### Uncategorized -- [#9608](https://github.com/MetaMask/metamask-extension/pull/9608): Ensure QR code scanner works -- [#9624](https://github.com/MetaMask/metamask-extension/pull/9624): Help users avoid insufficient gas prices in swaps -- [#9614](https://github.com/MetaMask/metamask-extension/pull/9614): Update swaps network fee tooltip -- [#9623](https://github.com/MetaMask/metamask-extension/pull/9623): Prevent reducing the gas limit for swaps -- [#9630](https://github.com/MetaMask/metamask-extension/pull/9630): Fix UI crash when trying to render estimated time remaining of non-submitted transaction -- [#9633](https://github.com/MetaMask/metamask-extension/pull/9633): Update View Quote page to better represent the MetaMask fee +- Ensure QR code scanner works ([#9608](https://github.com/MetaMask/metamask-extension/pull/9608)) +- Help users avoid insufficient gas prices in swaps ([#9624](https://github.com/MetaMask/metamask-extension/pull/9624)) +- Update swaps network fee tooltip ([#9614](https://github.com/MetaMask/metamask-extension/pull/9614)) +- Prevent reducing the gas limit for swaps ([#9623](https://github.com/MetaMask/metamask-extension/pull/9623)) +- Fix UI crash when trying to render estimated time remaining of non-submitted transaction ([#9630](https://github.com/MetaMask/metamask-extension/pull/9630)) +- Update View Quote page to better represent the MetaMask fee ([#9633](https://github.com/MetaMask/metamask-extension/pull/9633)) ## [8.1.1] - 2020-10-15 ### Uncategorized -- [#9586](https://github.com/MetaMask/metamask-extension/pull/9586): Prevent build quote crash when swapping from non-tracked token with balance (#9586) -- [#9592](https://github.com/MetaMask/metamask-extension/pull/9592): Remove commitment to maintain a public metrics dashboard (#9592) -- [#9596](https://github.com/MetaMask/metamask-extension/pull/9596): Fix TypeError when `signTypedData` throws (#9596) -- [#9591](https://github.com/MetaMask/metamask-extension/pull/9591): Fix Firefox overflow on transaction items with long amounts (#9591) -- [#9601](https://github.com/MetaMask/metamask-extension/pull/9601): Update text content of invalid custom network alert (#9601) -- [#9575](https://github.com/MetaMask/metamask-extension/pull/9575): Ensure proper hover display for accounts in main menu (#9575) -- [#9576](https://github.com/MetaMask/metamask-extension/pull/9576): Autofocus the appropriate text fields in the Create/Import/Hardware screen (#9576) -- [#9581](https://github.com/MetaMask/metamask-extension/pull/9581): AutoFocus the from input on swaps screen (#9581) -- [#9602](https://github.com/MetaMask/metamask-extension/pull/9602): Prevent swap button from being focused when disabled (#9602) -- [#9609](https://github.com/MetaMask/metamask-extension/pull/9609): Ensure swaps customize gas modal values are set correctly (#9609) +- Prevent build quote crash when swapping from non-tracked token with balance ([#9586](https://github.com/MetaMask/metamask-extension/pull/9586)) +- Remove commitment to maintain a public metrics dashboard ([#9592](https://github.com/MetaMask/metamask-extension/pull/9592)) +- Fix TypeError when `signTypedData` throws ([#9596](https://github.com/MetaMask/metamask-extension/pull/9596)) +- Fix Firefox overflow on transaction items with long amounts ([#9591](https://github.com/MetaMask/metamask-extension/pull/9591)) +- Update text content of invalid custom network alert ([#9601](https://github.com/MetaMask/metamask-extension/pull/9601)) +- Ensure proper hover display for accounts in main menu ([#9575](https://github.com/MetaMask/metamask-extension/pull/9575)) +- Autofocus the appropriate text fields in the Create/Import/Hardware screen ([#9576](https://github.com/MetaMask/metamask-extension/pull/9576)) +- AutoFocus the from input on swaps screen ([#9581](https://github.com/MetaMask/metamask-extension/pull/9581)) +- Prevent swap button from being focused when disabled ([#9602](https://github.com/MetaMask/metamask-extension/pull/9602)) +- Ensure swaps customize gas modal values are set correctly ([#9609](https://github.com/MetaMask/metamask-extension/pull/9609)) ## [8.1.0] - 2020-10-13 ### Uncategorized -- [#9565](https://github.com/MetaMask/metamask-extension/pull/9565): Ensure address book entries are shared between networks with the same chain ID -- [#9552](https://github.com/MetaMask/metamask-extension/pull/9552): Fix `eth_signTypedData_v4` chain ID validation for non-default networks -- [#9551](https://github.com/MetaMask/metamask-extension/pull/9551): Allow the "Localhost 8545" network to be edited, and require a chain ID to be specified for it -- [#9491](https://github.com/MetaMask/metamask-extension/pull/9491): Validate custom network chain IDs against endpoint `eth_chainId` return values -- [#9487](https://github.com/MetaMask/metamask-extension/pull/9487): Require chain IDs to be specified for custom networks -- [#9482](https://github.com/MetaMask/metamask-extension/pull/9482): Add MetaMask Swaps 🌻 -- [#9422](https://github.com/MetaMask/metamask-extension/pull/9422): Fix data backup feature (i.e. syncing with 3box) -- [#9434](https://github.com/MetaMask/metamask-extension/pull/9434): Improve gas input UI by using tooltip instead of a modal to communicate gas data -- [#9433](https://github.com/MetaMask/metamask-extension/pull/9433): Improve visual style and layout of the basic tab of the customize gas modal -- [#9415](https://github.com/MetaMask/metamask-extension/pull/9415): Fix UI bug in token approval confirmation notifications -- [#9414](https://github.com/MetaMask/metamask-extension/pull/9414): Update Wyre purchase URL -- [#9411](https://github.com/MetaMask/metamask-extension/pull/9411): Rename 'Ethereum Main Network' in network selector to 'Etherum Mainnet' -- [#9409](https://github.com/MetaMask/metamask-extension/pull/9409): Fix info tooltip on the alert settings screen when used in firefox -- [#9406](https://github.com/MetaMask/metamask-extension/pull/9406): Fix UI bug in customize gas modal: shwo left border when the first button is selected -- [#9395](https://github.com/MetaMask/metamask-extension/pull/9395): Correctly save new Contact Book addressed after editing them in 'Settings > Contact' -- [#9293](https://github.com/MetaMask/metamask-extension/pull/9293): Improve Italian translations -- [#9295](https://github.com/MetaMask/metamask-extension/pull/9295): Ensure the extension can be unlocked without network/internet access -- [#9344](https://github.com/MetaMask/metamask-extension/pull/9344): Add messages to Ledger connection process -- [#9329](https://github.com/MetaMask/metamask-extension/pull/9329): Hide seedphrase by default when restoring vault, and provide option for it to be shown -- [#9333](https://github.com/MetaMask/metamask-extension/pull/9333): Ensure names of token symbols are shown when token amounts in the token list are long -- [#9321](https://github.com/MetaMask/metamask-extension/pull/9321): Warn users when sending tokens to the token address -- [#9288](https://github.com/MetaMask/metamask-extension/pull/9288): Fix bug that caused the accounts list to be empty after entering an incorrect password when attempting to export private key -- [#9314](https://github.com/MetaMask/metamask-extension/pull/9314): Improve/fix error text for when ENS names are not found, on mainnet -- [#9307](https://github.com/MetaMask/metamask-extension/pull/9307): Improve 'Contact Us' copy in settings -- [#9283](https://github.com/MetaMask/metamask-extension/pull/9283): Fix capitalization of copy on MetaMetrics opt-in page -- [#9269](https://github.com/MetaMask/metamask-extension/pull/9269): Add lock icon to default networks in the Settings network page, to indicate they are not editable -- [#9189](https://github.com/MetaMask/metamask-extension/pull/9189): Hide gas price/speed estimate button, and link to advanced gas modal, in send flow on non-main network -- [#9184](https://github.com/MetaMask/metamask-extension/pull/9184): Improve visual styling of back button in account modal -- [#9152](https://github.com/MetaMask/metamask-extension/pull/9152): Fix vertical align of the network name in network dropdown button -- [#9073](https://github.com/MetaMask/metamask-extension/pull/9073): Use new Euclid font throughout MetaMask +- Ensure address book entries are shared between networks with the same chain ID ([#9565](https://github.com/MetaMask/metamask-extension/pull/9565)) +- Fix `eth_signTypedData_v4` chain ID validation for non-default networks ([#9552](https://github.com/MetaMask/metamask-extension/pull/9552)) +- Allow the "Localhost 8545" network to be edited, and require a chain ID to be specified for it ([#9551](https://github.com/MetaMask/metamask-extension/pull/9551)) +- Validate custom network chain IDs against endpoint `eth_chainId` return values ([#9491](https://github.com/MetaMask/metamask-extension/pull/9491)) +- Require chain IDs to be specified for custom networks ([#9487](https://github.com/MetaMask/metamask-extension/pull/9487)) +- Add MetaMask Swaps 🌻 ([#9482](https://github.com/MetaMask/metamask-extension/pull/9482)) +- Fix data backup feature ([#9422](https://github.com/MetaMask/metamask-extension/pull/9422)) +- Improve gas input UI by using tooltip instead of a modal to communicate gas data ([#9434](https://github.com/MetaMask/metamask-extension/pull/9434)) +- Improve visual style and layout of the basic tab of the customize gas modal ([#9433](https://github.com/MetaMask/metamask-extension/pull/9433)) +- Fix UI bug in token approval confirmation notifications ([#9415](https://github.com/MetaMask/metamask-extension/pull/9415)) +- Update Wyre purchase URL ([#9414](https://github.com/MetaMask/metamask-extension/pull/9414)) +- Rename 'Ethereum Main Network' in network selector to 'Etherum Mainnet' ([#9411](https://github.com/MetaMask/metamask-extension/pull/9411)) +- Fix info tooltip on the alert settings screen when used in firefox ([#9409](https://github.com/MetaMask/metamask-extension/pull/9409)) +- Fix UI bug in customize gas modal: shwo left border when the first button is selected ([#9406](https://github.com/MetaMask/metamask-extension/pull/9406)) +- Correctly save new Contact Book addressed after editing them in 'Settings > Contact' ([#9395](https://github.com/MetaMask/metamask-extension/pull/9395)) +- Improve Italian translations ([#9293](https://github.com/MetaMask/metamask-extension/pull/9293)) +- Ensure the extension can be unlocked without network/internet access ([#9295](https://github.com/MetaMask/metamask-extension/pull/9295)) +- Add messages to Ledger connection process ([#9344](https://github.com/MetaMask/metamask-extension/pull/9344)) +- Hide seedphrase by default when restoring vault, and provide option for it to be shown ([#9329](https://github.com/MetaMask/metamask-extension/pull/9329)) +- Ensure names of token symbols are shown when token amounts in the token list are long ([#9333](https://github.com/MetaMask/metamask-extension/pull/9333)) +- Warn users when sending tokens to the token address ([#9321](https://github.com/MetaMask/metamask-extension/pull/9321)) +- Fix bug that caused the accounts list to be empty after entering an incorrect password when attempting to export private key ([#9288](https://github.com/MetaMask/metamask-extension/pull/9288)) +- Improve/fix error text for when ENS names are not found, on mainnet ([#9314](https://github.com/MetaMask/metamask-extension/pull/9314)) +- Improve 'Contact Us' copy in settings ([#9307](https://github.com/MetaMask/metamask-extension/pull/9307)) +- Fix capitalization of copy on MetaMetrics opt-in page ([#9283](https://github.com/MetaMask/metamask-extension/pull/9283)) +- Add lock icon to default networks in the Settings network page, to indicate they are not editable ([#9269](https://github.com/MetaMask/metamask-extension/pull/9269)) +- Hide gas price/speed estimate button, and link to advanced gas modal, in send flow on non-main network ([#9189](https://github.com/MetaMask/metamask-extension/pull/9189)) +- Improve visual styling of back button in account modal ([#9184](https://github.com/MetaMask/metamask-extension/pull/9184)) +- Fix vertical align of the network name in network dropdown button ([#9152](https://github.com/MetaMask/metamask-extension/pull/9152)) +- Use new Euclid font throughout MetaMask ([#9073](https://github.com/MetaMask/metamask-extension/pull/9073)) ## [8.0.10] - 2020-09-16 ### Uncategorized -- [#9423](https://github.com/MetaMask/metamask-extension/pull/9423): Update default phishing list -- [#9416](https://github.com/MetaMask/metamask-extension/pull/9416): Fix fetching a new phishing list on Firefox +- Update default phishing list ([#9423](https://github.com/MetaMask/metamask-extension/pull/9423)) +- Fix fetching a new phishing list on Firefox ([#9416](https://github.com/MetaMask/metamask-extension/pull/9416)) ## [8.0.9] - 2020-08-19 ### Uncategorized -- [#9228](https://github.com/MetaMask/metamask-extension/pull/9228): Move transaction confirmation footer buttons to scrollable area -- [#9256](https://github.com/MetaMask/metamask-extension/pull/9256): Handle non-String web3 property access -- [#9266](https://github.com/MetaMask/metamask-extension/pull/9266): Use @metamask/controllers@2.0.5 -- [#9189](https://github.com/MetaMask/metamask-extension/pull/9189): Hide ETH Gas Station estimates on non-main network +- Move transaction confirmation footer buttons to scrollable area ([#9228](https://github.com/MetaMask/metamask-extension/pull/9228)) +- Handle non-String web3 property access ([#9256](https://github.com/MetaMask/metamask-extension/pull/9256)) +- Use @metamask/controllers@2.0.5 ([#9266](https://github.com/MetaMask/metamask-extension/pull/9266)) +- Hide ETH Gas Station estimates on non-main network ([#9189](https://github.com/MetaMask/metamask-extension/pull/9189)) ## [8.0.8] - 2020-08-14 ### Uncategorized -- [#9211](https://github.com/MetaMask/metamask-extension/pull/9211): Fix Etherscan redirect on notification click -- [#9237](https://github.com/MetaMask/metamask-extension/pull/9237): Reduce volume of web3 usage metrics -- [#9227](https://github.com/MetaMask/metamask-extension/pull/9227): Permit all-caps addresses +- Fix Etherscan redirect on notification click ([#9211](https://github.com/MetaMask/metamask-extension/pull/9211)) +- Reduce volume of web3 usage metrics ([#9237](https://github.com/MetaMask/metamask-extension/pull/9237)) +- Permit all-caps addresses ([#9227](https://github.com/MetaMask/metamask-extension/pull/9227)) ## [8.0.7] - 2020-08-10 ### Uncategorized -- [#9065](https://github.com/MetaMask/metamask-extension/pull/9065): Change title of "Reveal Seed Words" page to "Reveal Seed Phrase" -- [#8974](https://github.com/MetaMask/metamask-extension/pull/8974): Add tooltip to copy button for contacts and seed phrase -- [#9063](https://github.com/MetaMask/metamask-extension/pull/9063): Fix broken UI upon failed password validation -- [#9075](https://github.com/MetaMask/metamask-extension/pull/9075): Fix shifted popup notification when browser is in fullscreen on macOS -- [#9085](https://github.com/MetaMask/metamask-extension/pull/9085): Support longer text in network dropdown -- [#8873](https://github.com/MetaMask/metamask-extension/pull/8873): Fix onboarding bug where user can be asked to verify seed phrase twice -- [#9104](https://github.com/MetaMask/metamask-extension/pull/9104): Replace "Email us" button with "Contact us" button -- [#9137](https://github.com/MetaMask/metamask-extension/pull/9137): Fix bug where `accountsChanged` events stop after a dapp connection is closed. -- [#9152](https://github.com/MetaMask/metamask-extension/pull/9152): Fix network name alignment -- [#9144](https://github.com/MetaMask/metamask-extension/pull/9144): Add web3 usage metrics and prepare for web3 removal +- Change title of "Reveal Seed Words" page to "Reveal Seed Phrase" ([#9065](https://github.com/MetaMask/metamask-extension/pull/9065)) +- Add tooltip to copy button for contacts and seed phrase ([#8974](https://github.com/MetaMask/metamask-extension/pull/8974)) +- Fix broken UI upon failed password validation ([#9063](https://github.com/MetaMask/metamask-extension/pull/9063)) +- Fix shifted popup notification when browser is in fullscreen on macOS ([#9075](https://github.com/MetaMask/metamask-extension/pull/9075)) +- Support longer text in network dropdown ([#9085](https://github.com/MetaMask/metamask-extension/pull/9085)) +- Fix onboarding bug where user can be asked to verify seed phrase twice ([#8873](https://github.com/MetaMask/metamask-extension/pull/8873)) +- Replace "Email us" button with "Contact us" button ([#9104](https://github.com/MetaMask/metamask-extension/pull/9104)) +- Fix bug where `accountsChanged` events stop after a dapp connection is closed. ([#9137](https://github.com/MetaMask/metamask-extension/pull/9137)) +- Fix network name alignment ([#9152](https://github.com/MetaMask/metamask-extension/pull/9152)) +- Add web3 usage metrics and prepare for web3 removal ([#9144](https://github.com/MetaMask/metamask-extension/pull/9144)) ## [8.0.6] - 2020-07-23 ### Uncategorized -- [#9030](https://github.com/MetaMask/metamask-extension/pull/9030): Hide "delete" button when editing contact of wallet account -- [#9031](https://github.com/MetaMask/metamask-extension/pull/9031): Fix crash upon removing contact -- [#9032](https://github.com/MetaMask/metamask-extension/pull/9032): Do not show spend limit for approvals -- [#9046](https://github.com/MetaMask/metamask-extension/pull/9046): Update @metamask/inpage-provider@6.1.0 -- [#9048](https://github.com/MetaMask/metamask-extension/pull/9048): Skip attempts to resolve 0x contract prefix -- [#9051](https://github.com/MetaMask/metamask-extension/pull/9051): Use content-hash@2.5.2 -- [#9056](https://github.com/MetaMask/metamask-extension/pull/9056): Display at least one significant digit of small non-zero token balances +- Hide "delete" button when editing contact of wallet account ([#9030](https://github.com/MetaMask/metamask-extension/pull/9030)) +- Fix crash upon removing contact ([#9031](https://github.com/MetaMask/metamask-extension/pull/9031)) +- Do not show spend limit for approvals ([#9032](https://github.com/MetaMask/metamask-extension/pull/9032)) +- Update @metamask/inpage-provider@6.1.0 ([#9046](https://github.com/MetaMask/metamask-extension/pull/9046)) +- Skip attempts to resolve 0x contract prefix ([#9048](https://github.com/MetaMask/metamask-extension/pull/9048)) +- Use content-hash@2.5.2 ([#9051](https://github.com/MetaMask/metamask-extension/pull/9051)) +- Display at least one significant digit of small non-zero token balances ([#9056](https://github.com/MetaMask/metamask-extension/pull/9056)) ## [8.0.5] - 2020-07-17 ### Uncategorized -- [#8942](https://github.com/MetaMask/metamask-extension/pull/8942): Fix display of incoming transactions (#8942) -- [#8998](https://github.com/MetaMask/metamask-extension/pull/8998): Fix `web3_clientVersion` method (#8998) -- [#9003](https://github.com/MetaMask/metamask-extension/pull/9003): @metamask/inpage-provider@6.0.1 (#9003) -- [#9006](https://github.com/MetaMask/metamask-extension/pull/9006): Hide loading indication after `personal_sign` (#9006) -- [#9011](https://github.com/MetaMask/metamask-extension/pull/9011): Display pending notifications after connect flow (#9011) -- [#9012](https://github.com/MetaMask/metamask-extension/pull/9012): Skip render when home page is closing or redirecting (#9012) -- [#9010](https://github.com/MetaMask/metamask-extension/pull/9010): Limit number of transactions passed outside of TransactionController (#9010) -- [#9023](https://github.com/MetaMask/metamask-extension/pull/9023): Clear AccountTracker accounts and CachedBalances on createNewVaultAndRestore (#9023) -- [#9025](https://github.com/MetaMask/metamask-extension/pull/9025): Catch gas estimate errors (#9025) -- [#9026](https://github.com/MetaMask/metamask-extension/pull/9026): Clear transactions on createNewVaultAndRestore (#9026) +- Fix display of incoming transactions ([#8942](https://github.com/MetaMask/metamask-extension/pull/8942)) +- Fix `web3_clientVersion` method ([#8998](https://github.com/MetaMask/metamask-extension/pull/8998)) +- @metamask/inpage-provider@6.0.1 ([#9003](https://github.com/MetaMask/metamask-extension/pull/9003)) +- Hide loading indication after `personal_sign` ([#9006](https://github.com/MetaMask/metamask-extension/pull/9006)) +- Display pending notifications after connect flow ([#9011](https://github.com/MetaMask/metamask-extension/pull/9011)) +- Skip render when home page is closing or redirecting ([#9012](https://github.com/MetaMask/metamask-extension/pull/9012)) +- Limit number of transactions passed outside of TransactionController ([#9010](https://github.com/MetaMask/metamask-extension/pull/9010)) +- Clear AccountTracker accounts and CachedBalances on createNewVaultAndRestore ([#9023](https://github.com/MetaMask/metamask-extension/pull/9023)) +- Catch gas estimate errors ([#9025](https://github.com/MetaMask/metamask-extension/pull/9025)) +- Clear transactions on createNewVaultAndRestore ([#9026](https://github.com/MetaMask/metamask-extension/pull/9026)) ## [8.0.4] - 2020-07-08 ### Uncategorized -- [#8934](https://github.com/MetaMask/metamask-extension/pull/8934): Fix transaction activity on custom networks -- [#8936](https://github.com/MetaMask/metamask-extension/pull/8936): Fix account tracker optimization +- Fix transaction activity on custom networks ([#8934](https://github.com/MetaMask/metamask-extension/pull/8934)) +- Fix account tracker optimization ([#8936](https://github.com/MetaMask/metamask-extension/pull/8936)) ## [8.0.3] - 2020-07-06 ### Uncategorized -- [#8921](https://github.com/MetaMask/metamask-extension/pull/8921): Restore missing 'data' provider event, and fix 'notification' event -- [#8923](https://github.com/MetaMask/metamask-extension/pull/8923): Normalize the 'from' parameter for `eth_sendTransaction` -- [#8924](https://github.com/MetaMask/metamask-extension/pull/8924): Fix handling of multiple `eth_requestAccount` messages from the same domain -- [#8917](https://github.com/MetaMask/metamask-extension/pull/8917): Update Italian translations +- Restore missing 'data' provider event, and fix 'notification' event ([#8921](https://github.com/MetaMask/metamask-extension/pull/8921)) +- Normalize the 'from' parameter for `eth_sendTransaction` ([#8923](https://github.com/MetaMask/metamask-extension/pull/8923)) +- Fix handling of multiple `eth_requestAccount` messages from the same domain ([#8924](https://github.com/MetaMask/metamask-extension/pull/8924)) +- Update Italian translations ([#8917](https://github.com/MetaMask/metamask-extension/pull/8917)) ## [8.0.2] - 2020-07-03 ### Uncategorized -- [#8907](https://github.com/MetaMask/metamask-extension/pull/8907): Tolerate missing or falsey substitutions -- [#8908](https://github.com/MetaMask/metamask-extension/pull/8908): Fix activity log inline buttons -- [#8909](https://github.com/MetaMask/metamask-extension/pull/8909): Prevent confirming blank suggested token -- [#8910](https://github.com/MetaMask/metamask-extension/pull/8910): Handle suggested token resolved elsewhere -- [#8913](https://github.com/MetaMask/metamask-extension/pull/8913): Fix Kovan chain ID constant +- Tolerate missing or falsey substitutions ([#8907](https://github.com/MetaMask/metamask-extension/pull/8907)) +- Fix activity log inline buttons ([#8908](https://github.com/MetaMask/metamask-extension/pull/8908)) +- Prevent confirming blank suggested token ([#8909](https://github.com/MetaMask/metamask-extension/pull/8909)) +- Handle suggested token resolved elsewhere ([#8910](https://github.com/MetaMask/metamask-extension/pull/8910)) +- Fix Kovan chain ID constant ([#8913](https://github.com/MetaMask/metamask-extension/pull/8913)) ## [8.0.1] - 2020-07-02 ### Uncategorized -- [#8874](https://github.com/MetaMask/metamask-extension/pull/8874): Fx overflow behaviour of add token list -- [#8885](https://github.com/MetaMask/metamask-extension/pull/8885): Show `origin` in connect flow rather than site name -- [#8883](https://github.com/MetaMask/metamask-extension/pull/8883): Allow setting a custom nonce of zero -- [#8889](https://github.com/MetaMask/metamask-extension/pull/8889): Fix language code format mismatch -- [#8891](https://github.com/MetaMask/metamask-extension/pull/8891): Prevent showing connected accounts without origin -- [#8893](https://github.com/MetaMask/metamask-extension/pull/8893): Prevent manually connecting to extension UI -- [#8895](https://github.com/MetaMask/metamask-extension/pull/8895): Allow localized messages to not use substitutions -- [#8897](https://github.com/MetaMask/metamask-extension/pull/8897): Update eth-keyring-controller to fix erasure of imported/hardware account names -- [#8896](https://github.com/MetaMask/metamask-extension/pull/8896): Include relative time polyfill locale data -- [#8898](https://github.com/MetaMask/metamask-extension/pull/8898): Replace percentage opacity value +- Fx overflow behaviour of add token list ([#8874](https://github.com/MetaMask/metamask-extension/pull/8874)) +- Show `origin` in connect flow rather than site name ([#8885](https://github.com/MetaMask/metamask-extension/pull/8885)) +- Allow setting a custom nonce of zero ([#8883](https://github.com/MetaMask/metamask-extension/pull/8883)) +- Fix language code format mismatch ([#8889](https://github.com/MetaMask/metamask-extension/pull/8889)) +- Prevent showing connected accounts without origin ([#8891](https://github.com/MetaMask/metamask-extension/pull/8891)) +- Prevent manually connecting to extension UI ([#8893](https://github.com/MetaMask/metamask-extension/pull/8893)) +- Allow localized messages to not use substitutions ([#8895](https://github.com/MetaMask/metamask-extension/pull/8895)) +- Update eth-keyring-controller to fix erasure of imported/hardware account names ([#8897](https://github.com/MetaMask/metamask-extension/pull/8897)) +- Include relative time polyfill locale data ([#8896](https://github.com/MetaMask/metamask-extension/pull/8896)) +- Replace percentage opacity value ([#8898](https://github.com/MetaMask/metamask-extension/pull/8898)) ## [8.0.0] - 2020-07-01 ### Uncategorized -- [#7004](https://github.com/MetaMask/metamask-extension/pull/7004): Add permission system -- [#7261](https://github.com/MetaMask/metamask-extension/pull/7261): Search accounts by name -- [#7483](https://github.com/MetaMask/metamask-extension/pull/7483): Buffer 3 blocks before dropping a transaction -- [#7620](https://github.com/MetaMask/metamask-extension/pull/7620): Handle one specific permissions request per tab -- [#7686](https://github.com/MetaMask/metamask-extension/pull/7686): Add description to Reset Account in settings -- [#7362](https://github.com/MetaMask/metamask-extension/pull/7362): Allow custom IPFS gateway and use more secure default gateway -- [#7696](https://github.com/MetaMask/metamask-extension/pull/7696): Adjust colour of Reset Account button to reflect danger -- [#7602](https://github.com/MetaMask/metamask-extension/pull/7602): Support new onboarding library -- [#7672](https://github.com/MetaMask/metamask-extension/pull/7672): Update custom token symbol length restriction message -- [#7747](https://github.com/MetaMask/metamask-extension/pull/7747): Handle 'Enter' keypress on restore from seed screen -- [#7810](https://github.com/MetaMask/metamask-extension/pull/7810): Remove padding around advanced gas info icon -- [#7840](https://github.com/MetaMask/metamask-extension/pull/7840): Force background state update after removing an account -- [#7853](https://github.com/MetaMask/metamask-extension/pull/7853): Change "Log In/Out" terminology to "Unlock/Lock" -- [#7863](https://github.com/MetaMask/metamask-extension/pull/7863): Add mechanism to randomize seed phrase filename -- [#7933](https://github.com/MetaMask/metamask-extension/pull/7933): Sort seed phrase confirmation buttons alphabetically -- [#7987](https://github.com/MetaMask/metamask-extension/pull/7987): Add support for 24 word seed phrases -- [#7971](https://github.com/MetaMask/metamask-extension/pull/7971): Use contact name instead of address during send flow -- [#8050](https://github.com/MetaMask/metamask-extension/pull/8050): Add title attribute to transaction title -- [#7831](https://github.com/MetaMask/metamask-extension/pull/7831): Implement encrypt/decrypt feature -- [#8125](https://github.com/MetaMask/metamask-extension/pull/8125): Add setting for disabling Eth Phishing Detection -- [#8148](https://github.com/MetaMask/metamask-extension/pull/8148): Prevent external domains from submitting more than one perm request at a time -- [#8149](https://github.com/MetaMask/metamask-extension/pull/8149): Wait for extension unlock before processing eth_requestAccounts -- [#8201](https://github.com/MetaMask/metamask-extension/pull/8201): Add Idle Timeout for Sync with mobile -- [#8247](https://github.com/MetaMask/metamask-extension/pull/8247): Update Italian translation -- [#8246](https://github.com/MetaMask/metamask-extension/pull/8246): Make seed phrase import case-insensitive -- [#8254](https://github.com/MetaMask/metamask-extension/pull/8254): Convert Connected Sites page to modal -- [#8259](https://github.com/MetaMask/metamask-extension/pull/8259): Update token cell to show inline stale balance warning -- [#8264](https://github.com/MetaMask/metamask-extension/pull/8264): Move asset list to home tab on small screens -- [#8270](https://github.com/MetaMask/metamask-extension/pull/8270): Connected status indicator -- [#8078](https://github.com/MetaMask/metamask-extension/pull/8078): Allow selecting multiple accounts during connect flow -- [#8318](https://github.com/MetaMask/metamask-extension/pull/8318): Focus the notification popup if it's already open -- [#8356](https://github.com/MetaMask/metamask-extension/pull/8356): Position notification relative to last focused window -- [#8358](https://github.com/MetaMask/metamask-extension/pull/8358): Close notification UI if no unapproved confirmations -- [#8293](https://github.com/MetaMask/metamask-extension/pull/8293): Add popup explaining connection indicator to existing users -- [#8435](https://github.com/MetaMask/metamask-extension/pull/8435): Correctly detect changes to background state -- [#7912](https://github.com/MetaMask/metamask-extension/pull/7912): Disable import button for empty string/file -- [#8246](https://github.com/MetaMask/metamask-extension/pull/8246): Make seed phrase import case-insensitive -- [#8312](https://github.com/MetaMask/metamask-extension/pull/8312): Alert user upon switching to unconnected account -- [#8445](https://github.com/MetaMask/metamask-extension/pull/8445): Only updating pending transactions upon block update -- [#8467](https://github.com/MetaMask/metamask-extension/pull/8467): Fix firefox popup location -- [#8486](https://github.com/MetaMask/metamask-extension/pull/8486): Prevent race condition where transaction value set in UI is overwritten -- [#8490](https://github.com/MetaMask/metamask-extension/pull/8490): Fix default gas race condition -- [#8491](https://github.com/MetaMask/metamask-extension/pull/8491): Update tokens after importing account -- [#8496](https://github.com/MetaMask/metamask-extension/pull/8496): Enable disconnecting a single account or all accounts -- [#8502](https://github.com/MetaMask/metamask-extension/pull/8502): Add support for IPFS address resolution -- [#8419](https://github.com/MetaMask/metamask-extension/pull/8419): Add version dimension to metrics event -- [#8508](https://github.com/MetaMask/metamask-extension/pull/8508): Open notification UI when eth_requestAccounts waits for unlock -- [#8533](https://github.com/MetaMask/metamask-extension/pull/8533): Prevent negative values on gas inputs -- [#8550](https://github.com/MetaMask/metamask-extension/pull/8550): Allow disabling alerts -- [#8563](https://github.com/MetaMask/metamask-extension/pull/8563): Synchronously update transaction status -- [#8567](https://github.com/MetaMask/metamask-extension/pull/8567): Improve Spanish localized message -- [#8532](https://github.com/MetaMask/metamask-extension/pull/8532): Add switch to connected account alert -- [#8575](https://github.com/MetaMask/metamask-extension/pull/8575): Stop polling for recent blocks on custom networks when UI is closed -- [#8579](https://github.com/MetaMask/metamask-extension/pull/8579): Fix Matomo dimension IDs -- [#8592](https://github.com/MetaMask/metamask-extension/pull/8592): Handle trailing / in block explorer URLs -- [#8313](https://github.com/MetaMask/metamask-extension/pull/8313): Add Connected Accounts modal -- [#8609](https://github.com/MetaMask/metamask-extension/pull/8609): Sticky position the tabs at the top -- [#8634](https://github.com/MetaMask/metamask-extension/pull/8634): Define global `web3` as non-enumerable -- [#8601](https://github.com/MetaMask/metamask-extension/pull/8601): warn user when sending from different account -- [#8612](https://github.com/MetaMask/metamask-extension/pull/8612): Persist home tab state -- [#8564](https://github.com/MetaMask/metamask-extension/pull/8564): Implement new transaction list design -- [#8596](https://github.com/MetaMask/metamask-extension/pull/8596): Restrict the size of the permissions metadata store -- [#8654](https://github.com/MetaMask/metamask-extension/pull/8654): Update account options menu design -- [#8657](https://github.com/MetaMask/metamask-extension/pull/8657): Implement new fullscreen design -- [#8663](https://github.com/MetaMask/metamask-extension/pull/8663): Show hostname in the disconnect confirmation -- [#8665](https://github.com/MetaMask/metamask-extension/pull/8665): Make address display wider in Account Details -- [#8670](https://github.com/MetaMask/metamask-extension/pull/8670): Fix token `decimal` type -- [#8653](https://github.com/MetaMask/metamask-extension/pull/8653): Limit Dapp permissions to primary account -- [#8666](https://github.com/MetaMask/metamask-extension/pull/8666): Manually connect via the full connect flow -- [#8677](https://github.com/MetaMask/metamask-extension/pull/8677): Add metrics events for Wyre and CoinSwitch -- [#8680](https://github.com/MetaMask/metamask-extension/pull/8680): Fix connect hardware styling -- [#8689](https://github.com/MetaMask/metamask-extension/pull/8689): Fix create account form styling -- [#8702](https://github.com/MetaMask/metamask-extension/pull/8702): Fix tab content disappearing during scrolling on macOS Firefox -- [#8696](https://github.com/MetaMask/metamask-extension/pull/8696): Implement asset page -- [#8716](https://github.com/MetaMask/metamask-extension/pull/8716): Add nonce to transaction details -- [#8717](https://github.com/MetaMask/metamask-extension/pull/8717): Use URL origin instead of hostname for permission domains -- [#8747](https://github.com/MetaMask/metamask-extension/pull/8747): Fix account menu entry for imported accounts -- [#8768](https://github.com/MetaMask/metamask-extension/pull/8768): Permissions: Do not display HTTP/HTTPS URL schemes for unique hosts -- [#8730](https://github.com/MetaMask/metamask-extension/pull/8730): Hide seed phrase during Account Import -- [#8785](https://github.com/MetaMask/metamask-extension/pull/8785): Rename 'History' tab to 'Activity' -- [#8781](https://github.com/MetaMask/metamask-extension/pull/8781): use UI button for add token functionality -- [#8786](https://github.com/MetaMask/metamask-extension/pull/8786): Show fiat amounts inline on token transfers -- [#8789](https://github.com/MetaMask/metamask-extension/pull/8789): Warn users to only add custom networks that they trust -- [#8802](https://github.com/MetaMask/metamask-extension/pull/8802): Consolidate connected account alerts -- [#8810](https://github.com/MetaMask/metamask-extension/pull/8810): Remove all user- and translator-facing instances of 'dapp' -- [#8836](https://github.com/MetaMask/metamask-extension/pull/8836): Update method data when cached method data is empty -- [#8833](https://github.com/MetaMask/metamask-extension/pull/8833): Improve error handling when signature requested without a keyholder address -- [#8850](https://github.com/MetaMask/metamask-extension/pull/8850): Stop upper-casing exported private key -- [#8631](https://github.com/MetaMask/metamask-extension/pull/8631): Include imported accounts in mobile sync +- Add permission system ([#7004](https://github.com/MetaMask/metamask-extension/pull/7004)) +- Search accounts by name ([#7261](https://github.com/MetaMask/metamask-extension/pull/7261)) +- Buffer 3 blocks before dropping a transaction ([#7483](https://github.com/MetaMask/metamask-extension/pull/7483)) +- Handle one specific permissions request per tab ([#7620](https://github.com/MetaMask/metamask-extension/pull/7620)) +- Add description to Reset Account in settings ([#7686](https://github.com/MetaMask/metamask-extension/pull/7686)) +- Allow custom IPFS gateway and use more secure default gateway ([#7362](https://github.com/MetaMask/metamask-extension/pull/7362)) +- Adjust colour of Reset Account button to reflect danger ([#7696](https://github.com/MetaMask/metamask-extension/pull/7696)) +- Support new onboarding library ([#7602](https://github.com/MetaMask/metamask-extension/pull/7602)) +- Update custom token symbol length restriction message ([#7672](https://github.com/MetaMask/metamask-extension/pull/7672)) +- Handle 'Enter' keypress on restore from seed screen ([#7747](https://github.com/MetaMask/metamask-extension/pull/7747)) +- Remove padding around advanced gas info icon ([#7810](https://github.com/MetaMask/metamask-extension/pull/7810)) +- Force background state update after removing an account ([#7840](https://github.com/MetaMask/metamask-extension/pull/7840)) +- Change "Log In/Out" terminology to "Unlock/Lock" ([#7853](https://github.com/MetaMask/metamask-extension/pull/7853)) +- Add mechanism to randomize seed phrase filename ([#7863](https://github.com/MetaMask/metamask-extension/pull/7863)) +- Sort seed phrase confirmation buttons alphabetically ([#7933](https://github.com/MetaMask/metamask-extension/pull/7933)) +- Add support for 24 word seed phrases ([#7987](https://github.com/MetaMask/metamask-extension/pull/7987)) +- Use contact name instead of address during send flow ([#7971](https://github.com/MetaMask/metamask-extension/pull/7971)) +- Add title attribute to transaction title ([#8050](https://github.com/MetaMask/metamask-extension/pull/8050)) +- Implement encrypt/decrypt feature ([#7831](https://github.com/MetaMask/metamask-extension/pull/7831)) +- Add setting for disabling Eth Phishing Detection ([#8125](https://github.com/MetaMask/metamask-extension/pull/8125)) +- Prevent external domains from submitting more than one perm request at a time ([#8148](https://github.com/MetaMask/metamask-extension/pull/8148)) +- Wait for extension unlock before processing eth_requestAccounts ([#8149](https://github.com/MetaMask/metamask-extension/pull/8149)) +- Add Idle Timeout for Sync with mobile ([#8201](https://github.com/MetaMask/metamask-extension/pull/8201)) +- Update Italian translation ([#8247](https://github.com/MetaMask/metamask-extension/pull/8247)) +- Make seed phrase import case-insensitive ([#8246](https://github.com/MetaMask/metamask-extension/pull/8246)) +- Convert Connected Sites page to modal ([#8254](https://github.com/MetaMask/metamask-extension/pull/8254)) +- Update token cell to show inline stale balance warning ([#8259](https://github.com/MetaMask/metamask-extension/pull/8259)) +- Move asset list to home tab on small screens ([#8264](https://github.com/MetaMask/metamask-extension/pull/8264)) +- Connected status indicator ([#8270](https://github.com/MetaMask/metamask-extension/pull/8270)) +- Allow selecting multiple accounts during connect flow ([#8078](https://github.com/MetaMask/metamask-extension/pull/8078)) +- Focus the notification popup if it's already open ([#8318](https://github.com/MetaMask/metamask-extension/pull/8318)) +- Position notification relative to last focused window ([#8356](https://github.com/MetaMask/metamask-extension/pull/8356)) +- Close notification UI if no unapproved confirmations ([#8358](https://github.com/MetaMask/metamask-extension/pull/8358)) +- Add popup explaining connection indicator to existing users ([#8293](https://github.com/MetaMask/metamask-extension/pull/8293)) +- Correctly detect changes to background state ([#8435](https://github.com/MetaMask/metamask-extension/pull/8435)) +- Disable import button for empty string/file ([#7912](https://github.com/MetaMask/metamask-extension/pull/7912)) +- Make seed phrase import case-insensitive ([#8246](https://github.com/MetaMask/metamask-extension/pull/8246)) +- Alert user upon switching to unconnected account ([#8312](https://github.com/MetaMask/metamask-extension/pull/8312)) +- Only updating pending transactions upon block update ([#8445](https://github.com/MetaMask/metamask-extension/pull/8445)) +- Fix firefox popup location ([#8467](https://github.com/MetaMask/metamask-extension/pull/8467)) +- Prevent race condition where transaction value set in UI is overwritten ([#8486](https://github.com/MetaMask/metamask-extension/pull/8486)) +- Fix default gas race condition ([#8490](https://github.com/MetaMask/metamask-extension/pull/8490)) +- Update tokens after importing account ([#8491](https://github.com/MetaMask/metamask-extension/pull/8491)) +- Enable disconnecting a single account or all accounts ([#8496](https://github.com/MetaMask/metamask-extension/pull/8496)) +- Add support for IPFS address resolution ([#8502](https://github.com/MetaMask/metamask-extension/pull/8502)) +- Add version dimension to metrics event ([#8419](https://github.com/MetaMask/metamask-extension/pull/8419)) +- Open notification UI when eth_requestAccounts waits for unlock ([#8508](https://github.com/MetaMask/metamask-extension/pull/8508)) +- Prevent negative values on gas inputs ([#8533](https://github.com/MetaMask/metamask-extension/pull/8533)) +- Allow disabling alerts ([#8550](https://github.com/MetaMask/metamask-extension/pull/8550)) +- Synchronously update transaction status ([#8563](https://github.com/MetaMask/metamask-extension/pull/8563)) +- Improve Spanish localized message ([#8567](https://github.com/MetaMask/metamask-extension/pull/8567)) +- Add switch to connected account alert ([#8532](https://github.com/MetaMask/metamask-extension/pull/8532)) +- Stop polling for recent blocks on custom networks when UI is closed ([#8575](https://github.com/MetaMask/metamask-extension/pull/8575)) +- Fix Matomo dimension IDs ([#8579](https://github.com/MetaMask/metamask-extension/pull/8579)) +- Handle trailing / in block explorer URLs ([#8592](https://github.com/MetaMask/metamask-extension/pull/8592)) +- Add Connected Accounts modal ([#8313](https://github.com/MetaMask/metamask-extension/pull/8313)) +- Sticky position the tabs at the top ([#8609](https://github.com/MetaMask/metamask-extension/pull/8609)) +- Define global `web3` as non-enumerable ([#8634](https://github.com/MetaMask/metamask-extension/pull/8634)) +- warn user when sending from different account ([#8601](https://github.com/MetaMask/metamask-extension/pull/8601)) +- Persist home tab state ([#8612](https://github.com/MetaMask/metamask-extension/pull/8612)) +- Implement new transaction list design ([#8564](https://github.com/MetaMask/metamask-extension/pull/8564)) +- Restrict the size of the permissions metadata store ([#8596](https://github.com/MetaMask/metamask-extension/pull/8596)) +- Update account options menu design ([#8654](https://github.com/MetaMask/metamask-extension/pull/8654)) +- Implement new fullscreen design ([#8657](https://github.com/MetaMask/metamask-extension/pull/8657)) +- Show hostname in the disconnect confirmation ([#8663](https://github.com/MetaMask/metamask-extension/pull/8663)) +- Make address display wider in Account Details ([#8665](https://github.com/MetaMask/metamask-extension/pull/8665)) +- Fix token `decimal` type ([#8670](https://github.com/MetaMask/metamask-extension/pull/8670)) +- Limit Dapp permissions to primary account ([#8653](https://github.com/MetaMask/metamask-extension/pull/8653)) +- Manually connect via the full connect flow ([#8666](https://github.com/MetaMask/metamask-extension/pull/8666)) +- Add metrics events for Wyre and CoinSwitch ([#8677](https://github.com/MetaMask/metamask-extension/pull/8677)) +- Fix connect hardware styling ([#8680](https://github.com/MetaMask/metamask-extension/pull/8680)) +- Fix create account form styling ([#8689](https://github.com/MetaMask/metamask-extension/pull/8689)) +- Fix tab content disappearing during scrolling on macOS Firefox ([#8702](https://github.com/MetaMask/metamask-extension/pull/8702)) +- Implement asset page ([#8696](https://github.com/MetaMask/metamask-extension/pull/8696)) +- Add nonce to transaction details ([#8716](https://github.com/MetaMask/metamask-extension/pull/8716)) +- Use URL origin instead of hostname for permission domains ([#8717](https://github.com/MetaMask/metamask-extension/pull/8717)) +- Fix account menu entry for imported accounts ([#8747](https://github.com/MetaMask/metamask-extension/pull/8747)) +- Permissions: Do not display HTTP/HTTPS URL schemes for unique hosts ([#8768](https://github.com/MetaMask/metamask-extension/pull/8768)) +- Hide seed phrase during Account Import ([#8730](https://github.com/MetaMask/metamask-extension/pull/8730)) +- Rename 'History' tab to 'Activity' ([#8785](https://github.com/MetaMask/metamask-extension/pull/8785)) +- use UI button for add token functionality ([#8781](https://github.com/MetaMask/metamask-extension/pull/8781)) +- Show fiat amounts inline on token transfers ([#8786](https://github.com/MetaMask/metamask-extension/pull/8786)) +- Warn users to only add custom networks that they trust ([#8789](https://github.com/MetaMask/metamask-extension/pull/8789)) +- Consolidate connected account alerts ([#8802](https://github.com/MetaMask/metamask-extension/pull/8802)) +- Remove all user- and translator-facing instances of 'dapp' ([#8810](https://github.com/MetaMask/metamask-extension/pull/8810)) +- Update method data when cached method data is empty ([#8836](https://github.com/MetaMask/metamask-extension/pull/8836)) +- Improve error handling when signature requested without a keyholder address ([#8833](https://github.com/MetaMask/metamask-extension/pull/8833)) +- Stop upper-casing exported private key ([#8850](https://github.com/MetaMask/metamask-extension/pull/8850)) +- Include imported accounts in mobile sync ([#8631](https://github.com/MetaMask/metamask-extension/pull/8631)) ## [7.7.9] - 2020-05-04 ### Uncategorized -- [#8446](https://github.com/MetaMask/metamask-extension/pull/8446): Fix popup not opening -- [#8449](https://github.com/MetaMask/metamask-extension/pull/8449): Skip adding history entry for empty txMeta diffs -- [#8447](https://github.com/MetaMask/metamask-extension/pull/8447): Delete Dai/Sai migration notification -- [#8460](https://github.com/MetaMask/metamask-extension/pull/8460): Update deposit copy for Wyre -- [#8458](https://github.com/MetaMask/metamask-extension/pull/8458): Snapshot txMeta without cloning history -- [#8459](https://github.com/MetaMask/metamask-extension/pull/8459): Fix method registry initialization -- [#8455](https://github.com/MetaMask/metamask-extension/pull/8455): Add Dai/Sai to currency display -- [#8461](https://github.com/MetaMask/metamask-extension/pull/8461): Prevent network switch upon close of network timeout overlay -- [#8457](https://github.com/MetaMask/metamask-extension/pull/8457): Add INR currency option -- [#8462](https://github.com/MetaMask/metamask-extension/pull/8462): Fix display of Kovan and Rinkeby chain IDs -- [#8465](https://github.com/MetaMask/metamask-extension/pull/8465): Use ethereum-ens-network-map for network support -- [#8463](https://github.com/MetaMask/metamask-extension/pull/8463): Update deprecated Etherscam link -- [#8474](https://github.com/MetaMask/metamask-extension/pull/8474): Only update pending transactions upon block update -- [#8476](https://github.com/MetaMask/metamask-extension/pull/8476): Update eth-contract-metadata -- [#8509](https://github.com/MetaMask/metamask-extension/pull/8509): Fix Tohen Typo +- Fix popup not opening ([#8446](https://github.com/MetaMask/metamask-extension/pull/8446)) +- Skip adding history entry for empty txMeta diffs ([#8449](https://github.com/MetaMask/metamask-extension/pull/8449)) +- Delete Dai/Sai migration notification ([#8447](https://github.com/MetaMask/metamask-extension/pull/8447)) +- Update deposit copy for Wyre ([#8460](https://github.com/MetaMask/metamask-extension/pull/8460)) +- Snapshot txMeta without cloning history ([#8458](https://github.com/MetaMask/metamask-extension/pull/8458)) +- Fix method registry initialization ([#8459](https://github.com/MetaMask/metamask-extension/pull/8459)) +- Add Dai/Sai to currency display ([#8455](https://github.com/MetaMask/metamask-extension/pull/8455)) +- Prevent network switch upon close of network timeout overlay ([#8461](https://github.com/MetaMask/metamask-extension/pull/8461)) +- Add INR currency option ([#8457](https://github.com/MetaMask/metamask-extension/pull/8457)) +- Fix display of Kovan and Rinkeby chain IDs ([#8462](https://github.com/MetaMask/metamask-extension/pull/8462)) +- Use ethereum-ens-network-map for network support ([#8465](https://github.com/MetaMask/metamask-extension/pull/8465)) +- Update deprecated Etherscam link ([#8463](https://github.com/MetaMask/metamask-extension/pull/8463)) +- Only update pending transactions upon block update ([#8474](https://github.com/MetaMask/metamask-extension/pull/8474)) +- Update eth-contract-metadata ([#8476](https://github.com/MetaMask/metamask-extension/pull/8476)) +- Fix Tohen Typo ([#8509](https://github.com/MetaMask/metamask-extension/pull/8509)) ## [7.7.8] - 2020-03-13 ### Uncategorized -- [#8176](https://github.com/MetaMask/metamask-extension/pull/8176): Handle and set gas estimation when max mode is clicked -- [#8178](https://github.com/MetaMask/metamask-extension/pull/8178): Use specified gas limit when speeding up a transaction +- Handle and set gas estimation when max mode is clicked ([#8176](https://github.com/MetaMask/metamask-extension/pull/8176)) +- Use specified gas limit when speeding up a transaction ([#8178](https://github.com/MetaMask/metamask-extension/pull/8178)) ## [7.7.7] - 2020-03-04 ### Uncategorized -- [#8162](https://github.com/MetaMask/metamask-extension/pull/8162): Remove invalid Ledger accounts -- [#8163](https://github.com/MetaMask/metamask-extension/pull/8163): Fix account index check +- Remove invalid Ledger accounts ([#8162](https://github.com/MetaMask/metamask-extension/pull/8162)) +- Fix account index check ([#8163](https://github.com/MetaMask/metamask-extension/pull/8163)) ## [7.7.6] - 2020-03-03 ### Uncategorized -- [#8154](https://github.com/MetaMask/metamask-extension/pull/8154): Prevent signing from incorrect Ledger account +- Prevent signing from incorrect Ledger account ([#8154](https://github.com/MetaMask/metamask-extension/pull/8154)) ## [7.7.5] - 2020-02-18 ### Uncategorized -- [#8053](https://github.com/MetaMask/metamask-extension/pull/8053): Inline the source text not the binary encoding for inpage script -- [#8049](https://github.com/MetaMask/metamask-extension/pull/8049): Add warning to watchAsset API when editing a known token -- [#8051](https://github.com/MetaMask/metamask-extension/pull/8051): Update Wyre ETH purchase url -- [#8059](https://github.com/MetaMask/metamask-extension/pull/8059): Attempt ENS resolution on any valid domain name +- Inline the source text not the binary encoding for inpage script ([#8053](https://github.com/MetaMask/metamask-extension/pull/8053)) +- Add warning to watchAsset API when editing a known token ([#8049](https://github.com/MetaMask/metamask-extension/pull/8049)) +- Update Wyre ETH purchase url ([#8051](https://github.com/MetaMask/metamask-extension/pull/8051)) +- Attempt ENS resolution on any valid domain name ([#8059](https://github.com/MetaMask/metamask-extension/pull/8059)) ## [7.7.4] - 2020-01-31 ### Uncategorized -- [#7918](https://github.com/MetaMask/metamask-extension/pull/7918): Update data on Approve screen after updating custom spend limit -- [#7919](https://github.com/MetaMask/metamask-extension/pull/7919): Allow editing max spend limit -- [#7920](https://github.com/MetaMask/metamask-extension/pull/7920): Validate custom spend limit -- [#7944](https://github.com/MetaMask/metamask-extension/pull/7944): Only resolve ENS on mainnet -- [#7954](https://github.com/MetaMask/metamask-extension/pull/7954): Update ENS registry addresses +- Update data on Approve screen after updating custom spend limit ([#7918](https://github.com/MetaMask/metamask-extension/pull/7918)) +- Allow editing max spend limit ([#7919](https://github.com/MetaMask/metamask-extension/pull/7919)) +- Validate custom spend limit ([#7920](https://github.com/MetaMask/metamask-extension/pull/7920)) +- Only resolve ENS on mainnet ([#7944](https://github.com/MetaMask/metamask-extension/pull/7944)) +- Update ENS registry addresses ([#7954](https://github.com/MetaMask/metamask-extension/pull/7954)) ## [7.7.3] - 2020-01-27 ### Uncategorized -- [#7894](https://github.com/MetaMask/metamask-extension/pull/7894): Update GABA dependency version -- [#7901](https://github.com/MetaMask/metamask-extension/pull/7901): Use eth-contract-metadata@1.12.1 -- [#7910](https://github.com/MetaMask/metamask-extension/pull/7910): Fixing broken JSON import help link +- Update GABA dependency version ([#7894](https://github.com/MetaMask/metamask-extension/pull/7894)) +- Use eth-contract-metadata@1.12.1 ([#7901](https://github.com/MetaMask/metamask-extension/pull/7901)) +- Fixing broken JSON import help link ([#7910](https://github.com/MetaMask/metamask-extension/pull/7910)) ## [7.7.2] - 2020-01-13 ### Uncategorized -- [#7753](https://github.com/MetaMask/metamask-extension/pull/7753): Fix gas estimate for tokens -- [#7473](https://github.com/MetaMask/metamask-extension/pull/7473): Fix transaction order on transaction confirmation screen +- Fix gas estimate for tokens ([#7753](https://github.com/MetaMask/metamask-extension/pull/7753)) +- Fix transaction order on transaction confirmation screen ([#7473](https://github.com/MetaMask/metamask-extension/pull/7473)) ## [7.7.1] - 2019-12-09 ### Uncategorized -- [#7488](https://github.com/MetaMask/metamask-extension/pull/7488): Fix text overlap when expanding transaction -- [#7491](https://github.com/MetaMask/metamask-extension/pull/7491): Update gas when asset is changed on send screen -- [#7500](https://github.com/MetaMask/metamask-extension/pull/7500): Remove unused onClick prop from Dropdown component -- [#7502](https://github.com/MetaMask/metamask-extension/pull/7502): Fix chainId for non standard networks -- [#7519](https://github.com/MetaMask/metamask-extension/pull/7519): Fixing hardware connect error display -- [#7501](https://github.com/MetaMask/metamask-extension/pull/7501): Fix accessibility of first-time-flow terms checkboxes -- [#7579](https://github.com/MetaMask/metamask-extension/pull/7579): Prevent Maker migration dismissal timeout state from being overwritten -- [#7581](https://github.com/MetaMask/metamask-extension/pull/7581): Persist Maker migration dismissal timeout -- [#7484](https://github.com/MetaMask/metamask-extension/pull/7484): Ensure transactions are shown in the order they are received -- [#7604](https://github.com/MetaMask/metamask-extension/pull/7604): Process URL fragment for ens-ipfs redirects -- [#7628](https://github.com/MetaMask/metamask-extension/pull/7628): Fix typo that resulted in degrated account menu performance -- [#7558](https://github.com/MetaMask/metamask-extension/pull/7558): Use localized messages for NotificationModal buttons +- Fix text overlap when expanding transaction ([#7488](https://github.com/MetaMask/metamask-extension/pull/7488)) +- Update gas when asset is changed on send screen ([#7491](https://github.com/MetaMask/metamask-extension/pull/7491)) +- Remove unused onClick prop from Dropdown component ([#7500](https://github.com/MetaMask/metamask-extension/pull/7500)) +- Fix chainId for non standard networks ([#7502](https://github.com/MetaMask/metamask-extension/pull/7502)) +- Fixing hardware connect error display ([#7519](https://github.com/MetaMask/metamask-extension/pull/7519)) +- Fix accessibility of first-time-flow terms checkboxes ([#7501](https://github.com/MetaMask/metamask-extension/pull/7501)) +- Prevent Maker migration dismissal timeout state from being overwritten ([#7579](https://github.com/MetaMask/metamask-extension/pull/7579)) +- Persist Maker migration dismissal timeout ([#7581](https://github.com/MetaMask/metamask-extension/pull/7581)) +- Ensure transactions are shown in the order they are received ([#7484](https://github.com/MetaMask/metamask-extension/pull/7484)) +- Process URL fragment for ens-ipfs redirects ([#7604](https://github.com/MetaMask/metamask-extension/pull/7604)) +- Fix typo that resulted in degrated account menu performance ([#7628](https://github.com/MetaMask/metamask-extension/pull/7628)) +- Use localized messages for NotificationModal buttons ([#7558](https://github.com/MetaMask/metamask-extension/pull/7558)) ## [7.7.0] - 2019-12-03 [WITHDRAWN] ### Uncategorized -- [#7004](https://github.com/MetaMask/metamask-extension/pull/7004): Connect distinct accounts per site -- [#7480](https://github.com/MetaMask/metamask-extension/pull/7480): Fixed link on root README.md -- [#7482](https://github.com/MetaMask/metamask-extension/pull/7482): Update Wyre ETH purchase url -- [#7484](https://github.com/MetaMask/metamask-extension/pull/7484): Ensure transactions are shown in the order they are received -- [#7491](https://github.com/MetaMask/metamask-extension/pull/7491): Update gas when token is changed on the send screen -- [#7501](https://github.com/MetaMask/metamask-extension/pull/7501): Fix accessibility of first-time-flow terms checkboxes -- [#7502](https://github.com/MetaMask/metamask-extension/pull/7502): Fix chainId for non standard networks -- [#7579](https://github.com/MetaMask/metamask-extension/pull/7579): Fix timing of DAI migration notifications after dismissal -- [#7519](https://github.com/MetaMask/metamask-extension/pull/7519): Fixing hardware connect error display -- [#7558](https://github.com/MetaMask/metamask-extension/pull/7558): Use localized messages for NotificationModal buttons -- [#7488](https://github.com/MetaMask/metamask-extension/pull/7488): Fix text overlap when expanding transaction +- Connect distinct accounts per site ([#7004](https://github.com/MetaMask/metamask-extension/pull/7004)) +- Fixed link on root README.md ([#7480](https://github.com/MetaMask/metamask-extension/pull/7480)) +- Update Wyre ETH purchase url ([#7482](https://github.com/MetaMask/metamask-extension/pull/7482)) +- Ensure transactions are shown in the order they are received ([#7484](https://github.com/MetaMask/metamask-extension/pull/7484)) +- Update gas when token is changed on the send screen ([#7491](https://github.com/MetaMask/metamask-extension/pull/7491)) +- Fix accessibility of first-time-flow terms checkboxes ([#7501](https://github.com/MetaMask/metamask-extension/pull/7501)) +- Fix chainId for non standard networks ([#7502](https://github.com/MetaMask/metamask-extension/pull/7502)) +- Fix timing of DAI migration notifications after dismissal ([#7579](https://github.com/MetaMask/metamask-extension/pull/7579)) +- Fixing hardware connect error display ([#7519](https://github.com/MetaMask/metamask-extension/pull/7519)) +- Use localized messages for NotificationModal buttons ([#7558](https://github.com/MetaMask/metamask-extension/pull/7558)) +- Fix text overlap when expanding transaction ([#7488](https://github.com/MetaMask/metamask-extension/pull/7488)) ## [7.6.1] - 2019-11-19 ### Uncategorized -- [#7475](https://github.com/MetaMask/metamask-extension/pull/7475): Add 'Remind Me Later' to the Maker notification -- [#7436](https://github.com/MetaMask/metamask-extension/pull/7436): Add additional rpcUrl verification -- [#7468](https://github.com/MetaMask/metamask-extension/pull/7468): Show transaction fee units on approve screen +- Add 'Remind Me Later' to the Maker notification ([#7475](https://github.com/MetaMask/metamask-extension/pull/7475)) +- Add additional rpcUrl verification ([#7436](https://github.com/MetaMask/metamask-extension/pull/7436)) +- Show transaction fee units on approve screen ([#7468](https://github.com/MetaMask/metamask-extension/pull/7468)) ## [7.6.0] - 2019-11-18 ### Uncategorized -- [#7450](https://github.com/MetaMask/metamask-extension/pull/7450): Add migration notification for users with non-zero Sai -- [#7461](https://github.com/MetaMask/metamask-extension/pull/7461): Import styles for showing multiple notifications -- [#7451](https://github.com/MetaMask/metamask-extension/pull/7451): Add button disabled when password is empty +- Add migration notification for users with non-zero Sai ([#7450](https://github.com/MetaMask/metamask-extension/pull/7450)) +- Import styles for showing multiple notifications ([#7461](https://github.com/MetaMask/metamask-extension/pull/7461)) +- Add button disabled when password is empty ([#7451](https://github.com/MetaMask/metamask-extension/pull/7451)) ## [7.5.3] - 2019-11-15 ### Uncategorized -- [#7412](https://github.com/MetaMask/metamask-extension/pull/7412): lock eth-contract-metadata (#7412) -- [#7416](https://github.com/MetaMask/metamask-extension/pull/7416): Add eslint import plugin to help detect unresolved paths -- [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound (#7414) -- [#7430](https://github.com/MetaMask/metamask-extension/pull/7430): Update badge colour -- [#7408](https://github.com/MetaMask/metamask-extension/pull/7408): Utilize the full size of icon space (#7408) -- [#7431](https://github.com/MetaMask/metamask-extension/pull/7431): Add all icons to manifest (#7431) -- [#7426](https://github.com/MetaMask/metamask-extension/pull/7426): Ensure Etherscan result is valid before reading it (#7426) -- [#7434](https://github.com/MetaMask/metamask-extension/pull/7434): Update 512px icon (#7434) -- [#7410](https://github.com/MetaMask/metamask-extension/pull/7410): Fix sourcemaps for Sentry -- [#7420](https://github.com/MetaMask/metamask-extension/pull/7420): Adds and end to end test for typed signature requests -- [#7439](https://github.com/MetaMask/metamask-extension/pull/7439): Add metricsEvent to contextTypes (#7439) -- [#7419](https://github.com/MetaMask/metamask-extension/pull/7419): Added webRequest.RequestFilter to filter main_frame .eth requests (#7419) +- lock eth-contract-metadata ([#7412](https://github.com/MetaMask/metamask-extension/pull/7412)) +- Add eslint import plugin to help detect unresolved paths ([#7416](https://github.com/MetaMask/metamask-extension/pull/7416)) +- Ensure SignatureRequestOriginal 'beforeunload' handler is bound ([#7414](https://github.com/MetaMask/metamask-extension/pull/7414)) +- Update badge colour ([#7430](https://github.com/MetaMask/metamask-extension/pull/7430)) +- Utilize the full size of icon space ([#7408](https://github.com/MetaMask/metamask-extension/pull/7408)) +- Add all icons to manifest ([#7431](https://github.com/MetaMask/metamask-extension/pull/7431)) +- Ensure Etherscan result is valid before reading it ([#7426](https://github.com/MetaMask/metamask-extension/pull/7426)) +- Update 512px icon ([#7434](https://github.com/MetaMask/metamask-extension/pull/7434)) +- Fix sourcemaps for Sentry ([#7410](https://github.com/MetaMask/metamask-extension/pull/7410)) +- Adds and end to end test for typed signature requests ([#7420](https://github.com/MetaMask/metamask-extension/pull/7420)) +- Add metricsEvent to contextTypes ([#7439](https://github.com/MetaMask/metamask-extension/pull/7439)) +- Added webRequest.RequestFilter to filter main_frame .eth requests ([#7419](https://github.com/MetaMask/metamask-extension/pull/7419)) ## [7.5.2] - 2019-11-14 ### Uncategorized -- [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound +- Ensure SignatureRequestOriginal 'beforeunload' handler is bound ([#7414](https://github.com/MetaMask/metamask-extension/pull/7414)) ## [7.5.1] - 2019-11-13 ### Uncategorized -- [#7402](https://github.com/MetaMask/metamask-extension/pull/7402): Fix regression for signed types data screens -- [#7390](https://github.com/MetaMask/metamask-extension/pull/7390): Update json-rpc-engine -- [#7401](https://github.com/MetaMask/metamask-extension/pull/7401): Reject connection request on window close +- Fix regression for signed types data screens ([#7402](https://github.com/MetaMask/metamask-extension/pull/7402)) +- Update json-rpc-engine ([#7390](https://github.com/MetaMask/metamask-extension/pull/7390)) +- Reject connection request on window close ([#7401](https://github.com/MetaMask/metamask-extension/pull/7401)) ## [7.5.0] - 2019-11-12 ### Uncategorized -- [#7328](https://github.com/MetaMask/metamask-extension/pull/7328): ignore known transactions on first broadcast and continue with normal flow -- [#7327](https://github.com/MetaMask/metamask-extension/pull/7327): eth_getTransactionByHash will now check metamask's local history for pending transactions -- [#7333](https://github.com/MetaMask/metamask-extension/pull/7333): Cleanup beforeunload handler after transaction is resolved -- [#7038](https://github.com/MetaMask/metamask-extension/pull/7038): Add support for ZeroNet -- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning -- [#6924](https://github.com/MetaMask/metamask-extension/pull/6924): Add Estimated time to pending tx -- [#7177](https://github.com/MetaMask/metamask-extension/pull/7177): ENS Reverse Resolution support -- [#6891](https://github.com/MetaMask/metamask-extension/pull/6891): New signature request v3 UI -- [#7348](https://github.com/MetaMask/metamask-extension/pull/7348): fix width in first time flow button -- [#7271](https://github.com/MetaMask/metamask-extension/pull/7271): Redesign approve screen -- [#7354](https://github.com/MetaMask/metamask-extension/pull/7354): fix account menu width -- [#7379](https://github.com/MetaMask/metamask-extension/pull/7379): Set default advanced tab gas limit -- [#7380](https://github.com/MetaMask/metamask-extension/pull/7380): Fix advanced tab gas chart -- [#7374](https://github.com/MetaMask/metamask-extension/pull/7374): Hide accounts dropdown scrollbars on Firefox -- [#7357](https://github.com/MetaMask/metamask-extension/pull/7357): Update to gaba@1.8.0 -- [#7335](https://github.com/MetaMask/metamask-extension/pull/7335): Add onbeforeunload and have it call onCancel +- ignore known transactions on first broadcast and continue with normal flow ([#7328](https://github.com/MetaMask/metamask-extension/pull/7328)) +- eth_getTransactionByHash will now check metamask's local history for pending transactions ([#7327](https://github.com/MetaMask/metamask-extension/pull/7327)) +- Cleanup beforeunload handler after transaction is resolved ([#7333](https://github.com/MetaMask/metamask-extension/pull/7333)) +- Add support for ZeroNet ([#7038](https://github.com/MetaMask/metamask-extension/pull/7038)) +- Add web3 deprecation warning ([#7334](https://github.com/MetaMask/metamask-extension/pull/7334)) +- Add Estimated time to pending tx ([#6924](https://github.com/MetaMask/metamask-extension/pull/6924)) +- ENS Reverse Resolution support ([#7177](https://github.com/MetaMask/metamask-extension/pull/7177)) +- New signature request v3 UI ([#6891](https://github.com/MetaMask/metamask-extension/pull/6891)) +- fix width in first time flow button ([#7348](https://github.com/MetaMask/metamask-extension/pull/7348)) +- Redesign approve screen ([#7271](https://github.com/MetaMask/metamask-extension/pull/7271)) +- fix account menu width ([#7354](https://github.com/MetaMask/metamask-extension/pull/7354)) +- Set default advanced tab gas limit ([#7379](https://github.com/MetaMask/metamask-extension/pull/7379)) +- Fix advanced tab gas chart ([#7380](https://github.com/MetaMask/metamask-extension/pull/7380)) +- Hide accounts dropdown scrollbars on Firefox ([#7374](https://github.com/MetaMask/metamask-extension/pull/7374)) +- Update to gaba@1.8.0 ([#7357](https://github.com/MetaMask/metamask-extension/pull/7357)) +- Add onbeforeunload and have it call onCancel ([#7335](https://github.com/MetaMask/metamask-extension/pull/7335)) ## [7.4.0] - 2019-11-04 ### Uncategorized -- [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent` -- [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings -- [#7306](https://github.com/MetaMask/metamask-extension/pull/7306): correct the zh-TW translation -- [#7309](https://github.com/MetaMask/metamask-extension/pull/7309): Freeze Promise global on boot -- [#7296](https://github.com/MetaMask/metamask-extension/pull/7296): Add "Retry" option for failed transactions -- [#7319](https://github.com/MetaMask/metamask-extension/pull/7319): Fix transaction list item status spacing issue -- [#7218](https://github.com/MetaMask/metamask-extension/pull/7218): Add hostname and extensionId to site metadata -- [#7324](https://github.com/MetaMask/metamask-extension/pull/7324): Fix contact deletion -- [#7326](https://github.com/MetaMask/metamask-extension/pull/7326): Fix edit contact details -- [#7325](https://github.com/MetaMask/metamask-extension/pull/7325): Update eth-json-rpc-filters to fix memory leak -- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning +- Use `AdvancedGasInputs` in `AdvancedTabContent` ([#7186](https://github.com/MetaMask/metamask-extension/pull/7186)) +- Move signTypedData signing out to keyrings ([#7304](https://github.com/MetaMask/metamask-extension/pull/7304)) +- correct the zh-TW translation ([#7306](https://github.com/MetaMask/metamask-extension/pull/7306)) +- Freeze Promise global on boot ([#7309](https://github.com/MetaMask/metamask-extension/pull/7309)) +- Add "Retry" option for failed transactions ([#7296](https://github.com/MetaMask/metamask-extension/pull/7296)) +- Fix transaction list item status spacing issue ([#7319](https://github.com/MetaMask/metamask-extension/pull/7319)) +- Add hostname and extensionId to site metadata ([#7218](https://github.com/MetaMask/metamask-extension/pull/7218)) +- Fix contact deletion ([#7324](https://github.com/MetaMask/metamask-extension/pull/7324)) +- Fix edit contact details ([#7326](https://github.com/MetaMask/metamask-extension/pull/7326)) +- Update eth-json-rpc-filters to fix memory leak ([#7325](https://github.com/MetaMask/metamask-extension/pull/7325)) +- Add web3 deprecation warning ([#7334](https://github.com/MetaMask/metamask-extension/pull/7334)) ## [7.3.1] - 2019-10-22 ### Uncategorized -- [#7298](https://github.com/MetaMask/metamask-extension/pull/7298): Turn off full screen vs popup a/b test +- Turn off full screen vs popup a/b test ([#7298](https://github.com/MetaMask/metamask-extension/pull/7298)) ## [7.3.0] - 2019-10-21 ### Uncategorized -- [#6972](https://github.com/MetaMask/metamask-extension/pull/6972): 3box integration -- [#7168](https://github.com/MetaMask/metamask-extension/pull/7168): Add fixes for German translations -- [#7170](https://github.com/MetaMask/metamask-extension/pull/7170): Remove the disk store -- [#7176](https://github.com/MetaMask/metamask-extension/pull/7176): Performance: Delivery optimized images -- [#7189](https://github.com/MetaMask/metamask-extension/pull/7189): add goerli to incoming tx -- [#7190](https://github.com/MetaMask/metamask-extension/pull/7190): Remove unused locale messages -- [#7173](https://github.com/MetaMask/metamask-extension/pull/7173): Fix RPC error messages -- [#7205](https://github.com/MetaMask/metamask-extension/pull/7205): address book entries by chainId -- [#7207](https://github.com/MetaMask/metamask-extension/pull/7207): obs-store/local-store should upgrade webextension error to real error -- [#7162](https://github.com/MetaMask/metamask-extension/pull/7162): Add a/b test for full screen transaction confirmations -- [#7089](https://github.com/MetaMask/metamask-extension/pull/7089): Add advanced setting to enable editing nonce on confirmation screens -- [#7239](https://github.com/MetaMask/metamask-extension/pull/7239): Update ETH logo, update deposit Ether logo height and width -- [#7255](https://github.com/MetaMask/metamask-extension/pull/7255): Use translated string for state log -- [#7266](https://github.com/MetaMask/metamask-extension/pull/7266): fix issue of xyz ens not resolving -- [#7253](https://github.com/MetaMask/metamask-extension/pull/7253): Prevent Logout Timer that's longer than a week. -- [#7285](https://github.com/MetaMask/metamask-extension/pull/7285): Lessen the length of ENS validation to 3 -- [#7287](https://github.com/MetaMask/metamask-extension/pull/7287): Fix phishing detect script +- 3box integration ([#6972](https://github.com/MetaMask/metamask-extension/pull/6972)) +- Add fixes for German translations ([#7168](https://github.com/MetaMask/metamask-extension/pull/7168)) +- Remove the disk store ([#7170](https://github.com/MetaMask/metamask-extension/pull/7170)) +- Performance: Delivery optimized images ([#7176](https://github.com/MetaMask/metamask-extension/pull/7176)) +- add goerli to incoming tx ([#7189](https://github.com/MetaMask/metamask-extension/pull/7189)) +- Remove unused locale messages ([#7190](https://github.com/MetaMask/metamask-extension/pull/7190)) +- Fix RPC error messages ([#7173](https://github.com/MetaMask/metamask-extension/pull/7173)) +- address book entries by chainId ([#7205](https://github.com/MetaMask/metamask-extension/pull/7205)) +- obs-store/local-store should upgrade webextension error to real error ([#7207](https://github.com/MetaMask/metamask-extension/pull/7207)) +- Add a/b test for full screen transaction confirmations ([#7162](https://github.com/MetaMask/metamask-extension/pull/7162)) +- Add advanced setting to enable editing nonce on confirmation screens ([#7089](https://github.com/MetaMask/metamask-extension/pull/7089)) +- Update ETH logo, update deposit Ether logo height and width ([#7239](https://github.com/MetaMask/metamask-extension/pull/7239)) +- Use translated string for state log ([#7255](https://github.com/MetaMask/metamask-extension/pull/7255)) +- fix issue of xyz ens not resolving ([#7266](https://github.com/MetaMask/metamask-extension/pull/7266)) +- Prevent Logout Timer that's longer than a week. ([#7253](https://github.com/MetaMask/metamask-extension/pull/7253)) +- Lessen the length of ENS validation to 3 ([#7285](https://github.com/MetaMask/metamask-extension/pull/7285)) +- Fix phishing detect script ([#7287](https://github.com/MetaMask/metamask-extension/pull/7287)) ## [7.2.3] - 2019-10-08 ### Uncategorized -- [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Fix gas limit when sending tx without data to a contract -- [#7260](https://github.com/MetaMask/metamask-extension/pull/7260): Do not transate on seed phrases -- [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Ensure correct tx category when sending to contracts without tx data +- Fix gas limit when sending tx without data to a contract ([#7252](https://github.com/MetaMask/metamask-extension/pull/7252)) +- Do not transate on seed phrases ([#7260](https://github.com/MetaMask/metamask-extension/pull/7260)) +- Ensure correct tx category when sending to contracts without tx data ([#7252](https://github.com/MetaMask/metamask-extension/pull/7252)) ## [7.2.2] - 2019-09-25 ### Uncategorized -- [#7213](https://github.com/MetaMask/metamask-extension/pull/7213): Update minimum Firefox verison to 56.0 +- Update minimum Firefox verison to 56.0 ([#7213](https://github.com/MetaMask/metamask-extension/pull/7213)) ## [7.2.1] - 2019-09-17 ### Uncategorized -- [#7180](https://github.com/MetaMask/metamask-extension/pull/7180): Add `appName` message to each locale +- Add `appName` message to each locale ([#7180](https://github.com/MetaMask/metamask-extension/pull/7180)) ## [7.2.0] - 2019-09-17 ### Uncategorized -- [#7099](https://github.com/MetaMask/metamask-extension/pull/7099): Update localization from Transifex Brave -- [#7137](https://github.com/MetaMask/metamask-extension/pull/7137): Fix validation of empty block explorer url's in custom network form -- [#7128](https://github.com/MetaMask/metamask-extension/pull/7128): Support for eth_signTypedData_v4 -- [#7110](https://github.com/MetaMask/metamask-extension/pull/7110): Adds `chaindIdChanged` event to the ethereum provider -- [#7091](https://github.com/MetaMask/metamask-extension/pull/7091): Improve browser performance issues caused by missing locale errors -- [#7085](https://github.com/MetaMask/metamask-extension/pull/7085): Prevent ineffectual speed ups of pending transactions that don't have the lowest nonce -- [#7156](https://github.com/MetaMask/metamask-extension/pull/7156): Set minimum Firefox version to v56.2 to support Waterfox -- [#7157](https://github.com/MetaMask/metamask-extension/pull/7157): Add polyfill for AbortController -- [#7161](https://github.com/MetaMask/metamask-extension/pull/7161): Replace `undefined` selectedAddress with `null` -- [#7171](https://github.com/MetaMask/metamask-extension/pull/7171): Fix recipient field of approve screen +- Update localization from Transifex Brave ([#7099](https://github.com/MetaMask/metamask-extension/pull/7099)) +- Fix validation of empty block explorer url's in custom network form ([#7137](https://github.com/MetaMask/metamask-extension/pull/7137)) +- Support for eth_signTypedData_v4 ([#7128](https://github.com/MetaMask/metamask-extension/pull/7128)) +- Adds `chaindIdChanged` event to the ethereum provider ([#7110](https://github.com/MetaMask/metamask-extension/pull/7110)) +- Improve browser performance issues caused by missing locale errors ([#7091](https://github.com/MetaMask/metamask-extension/pull/7091)) +- Prevent ineffectual speed ups of pending transactions that don't have the lowest nonce ([#7085](https://github.com/MetaMask/metamask-extension/pull/7085)) +- Set minimum Firefox version to v56.2 to support Waterfox ([#7156](https://github.com/MetaMask/metamask-extension/pull/7156)) +- Add polyfill for AbortController ([#7157](https://github.com/MetaMask/metamask-extension/pull/7157)) +- Replace `undefined` selectedAddress with `null` ([#7161](https://github.com/MetaMask/metamask-extension/pull/7161)) +- Fix recipient field of approve screen ([#7171](https://github.com/MetaMask/metamask-extension/pull/7171)) ## [7.1.1] - 2019-09-03 ### Uncategorized -- [#7059](https://github.com/MetaMask/metamask-extension/pull/7059): Remove blockscale, replace with ethgasstation -- [#7037](https://github.com/MetaMask/metamask-extension/pull/7037): Remove Babel 6 from internal dependencies -- [#7093](https://github.com/MetaMask/metamask-extension/pull/7093): Allow dismissing privacy mode notification from popup -- [#7087](https://github.com/MetaMask/metamask-extension/pull/7087): Add breadcrumb spacing on Contacts page -- [#7081](https://github.com/MetaMask/metamask-extension/pull/7081): Fix confirm token transaction amount display -- [#7088](https://github.com/MetaMask/metamask-extension/pull/7088): Fix BigNumber conversion error -- [#7072](https://github.com/MetaMask/metamask-extension/pull/7072): Right-to-left CSS (using module for conversion) -- [#6878](https://github.com/MetaMask/metamask-extension/pull/6878): Persian translation -- [#7012](https://github.com/MetaMask/metamask-extension/pull/7012): Added missed phrases to RU locale +- Remove blockscale, replace with ethgasstation ([#7059](https://github.com/MetaMask/metamask-extension/pull/7059)) +- Remove Babel 6 from internal dependencies ([#7037](https://github.com/MetaMask/metamask-extension/pull/7037)) +- Allow dismissing privacy mode notification from popup ([#7093](https://github.com/MetaMask/metamask-extension/pull/7093)) +- Add breadcrumb spacing on Contacts page ([#7087](https://github.com/MetaMask/metamask-extension/pull/7087)) +- Fix confirm token transaction amount display ([#7081](https://github.com/MetaMask/metamask-extension/pull/7081)) +- Fix BigNumber conversion error ([#7088](https://github.com/MetaMask/metamask-extension/pull/7088)) +- Right-to-left CSS ([#7072](https://github.com/MetaMask/metamask-extension/pull/7072)) +- Persian translation ([#6878](https://github.com/MetaMask/metamask-extension/pull/6878)) +- Added missed phrases to RU locale ([#7012](https://github.com/MetaMask/metamask-extension/pull/7012)) ## [7.1.0] - 2019-08-26 ### Uncategorized -- [#7035](https://github.com/MetaMask/metamask-extension/pull/7035): Filter non-ERC-20 assets during mobile sync (#7035) -- [#7021](https://github.com/MetaMask/metamask-extension/pull/7021): Using translated string for end of flow messaging (#7021) -- [#7018](https://github.com/MetaMask/metamask-extension/pull/7018): Rename Contacts List settings tab to Contacts (#7018) -- [#7013](https://github.com/MetaMask/metamask-extension/pull/7013): Connections settings tab (#7013) -- [#6996](https://github.com/MetaMask/metamask-extension/pull/6996): Fetch & display received transactions (#6996) -- [#6991](https://github.com/MetaMask/metamask-extension/pull/6991): Remove reload from Share Address button (#6991) -- [#6978](https://github.com/MetaMask/metamask-extension/pull/6978): Address book fixes (#6978) -- [#6944](https://github.com/MetaMask/metamask-extension/pull/6944): Show recipient alias in confirm header if exists (#6944) -- [#6930](https://github.com/MetaMask/metamask-extension/pull/6930): Add support for eth_signTypedData_v4 (#6930) -- [#7046](https://github.com/MetaMask/metamask-extension/pull/7046): Update Italian translation (#7046) -- [#7047](https://github.com/MetaMask/metamask-extension/pull/7047): Add warning about reload on network change +- Filter non-ERC-20 assets during mobile sync ([#7035](https://github.com/MetaMask/metamask-extension/pull/7035)) +- Using translated string for end of flow messaging ([#7021](https://github.com/MetaMask/metamask-extension/pull/7021)) +- Rename Contacts List settings tab to Contacts ([#7018](https://github.com/MetaMask/metamask-extension/pull/7018)) +- Connections settings tab ([#7013](https://github.com/MetaMask/metamask-extension/pull/7013)) +- Fetch & display received transactions ([#6996](https://github.com/MetaMask/metamask-extension/pull/6996)) +- Remove reload from Share Address button ([#6991](https://github.com/MetaMask/metamask-extension/pull/6991)) +- Address book fixes ([#6978](https://github.com/MetaMask/metamask-extension/pull/6978)) +- Show recipient alias in confirm header if exists ([#6944](https://github.com/MetaMask/metamask-extension/pull/6944)) +- Add support for eth_signTypedData_v4 ([#6930](https://github.com/MetaMask/metamask-extension/pull/6930)) +- Update Italian translation ([#7046](https://github.com/MetaMask/metamask-extension/pull/7046)) +- Add warning about reload on network change ([#7047](https://github.com/MetaMask/metamask-extension/pull/7047)) ## [7.0.1] - 2019-08-08 ### Uncategorized -- [#6975](https://github.com/MetaMask/metamask-extension/pull/6975): Ensure seed phrase backup notification only shows up for new users +- Ensure seed phrase backup notification only shows up for new users ([#6975](https://github.com/MetaMask/metamask-extension/pull/6975)) ## [7.0.0] - 2019-08-07 ### Uncategorized -- [#6828](https://github.com/MetaMask/metamask-extension/pull/6828): Capitalized speed up label to match rest of UI -- [#6874](https://github.com/MetaMask/metamask-extension/pull/6928): Allows skipping of seed phrase challenge during onboarding, and completing it at a later time -- [#6900](https://github.com/MetaMask/metamask-extension/pull/6900): Prevent opening of asset dropdown if no tokens in account -- [#6904](https://github.com/MetaMask/metamask-extension/pull/6904): Set privacy mode as default -- [#6914](https://github.com/MetaMask/metamask-extension/pull/6914): Adds Address Book feature -- [#6928](https://github.com/MetaMask/metamask-extension/pull/6928): Disable Copy Tx ID and block explorer link for transactions without hash -- [#6967](https://github.com/MetaMask/metamask-extension/pull/6967): Fix mobile sync +- Capitalized speed up label to match rest of UI ([#6828](https://github.com/MetaMask/metamask-extension/pull/6828)) +- Allows skipping of seed phrase challenge during onboarding, and completing it at a later time ([#6874](https://github.com/MetaMask/metamask-extension/pull/6928)) +- Prevent opening of asset dropdown if no tokens in account ([#6900](https://github.com/MetaMask/metamask-extension/pull/6900)) +- Set privacy mode as default ([#6904](https://github.com/MetaMask/metamask-extension/pull/6904)) +- Adds Address Book feature ([#6914](https://github.com/MetaMask/metamask-extension/pull/6914)) +- Disable Copy Tx ID and block explorer link for transactions without hash ([#6928](https://github.com/MetaMask/metamask-extension/pull/6928)) +- Fix mobile sync ([#6967](https://github.com/MetaMask/metamask-extension/pull/6967)) ## [6.7.3] - 2019-07-19 ### Uncategorized -- [#6888](https://github.com/MetaMask/metamask-extension/pull/6888): Fix bug with resubmitting unsigned transactions. +- Fix bug with resubmitting unsigned transactions. ([#6888](https://github.com/MetaMask/metamask-extension/pull/6888)) ## [6.7.2] - 2019-07-03 ### Uncategorized -- [#6713](https://github.com/MetaMask/metamask-extension/pull/6713): \* Normalize and Validate txParams in TransactionStateManager.addTx too -- [#6759](https://github.com/MetaMask/metamask-extension/pull/6759): Update to Node.js v10 -- [#6694](https://github.com/MetaMask/metamask-extension/pull/6694): Fixes #6694 -- [#6743](https://github.com/MetaMask/metamask-extension/pull/6743): \* Add tests for ImportWithSeedPhrase#parseSeedPhrase -- [#6740](https://github.com/MetaMask/metamask-extension/pull/6740): Fixes #6740 -- [#6741](https://github.com/MetaMask/metamask-extension/pull/6741): Fixes #6741 -- [#6761](https://github.com/MetaMask/metamask-extension/pull/6761): Fixes #6760, correct PropTypes for nextRoute -- [#6754](https://github.com/MetaMask/metamask-extension/pull/6754): Use inline source maps in development -- [#6589](https://github.com/MetaMask/metamask-extension/pull/6589): Document hotfix protocol -- [#6738](https://github.com/MetaMask/metamask-extension/pull/6738): Add codeowner for package-lock-old.json package-lock.json package.json packagelock-old.json files -- [#6648](https://github.com/MetaMask/metamask-extension/pull/6648): Add loading view to notification.html -- [#6731](https://github.com/MetaMask/metamask-extension/pull/6731): Add brave as a platform type for MetaMask +- Normalize and Validate txParams in TransactionStateManager.addTx too ([#6713](https://github.com/MetaMask/metamask-extension/pull/6713)) +- Update to Node.js v10 ([#6759](https://github.com/MetaMask/metamask-extension/pull/6759)) +- Fixes #6694 ([#6694](https://github.com/MetaMask/metamask-extension/pull/6694)) +- Add tests for ImportWithSeedPhrase#parseSeedPhrase ([#6743](https://github.com/MetaMask/metamask-extension/pull/6743)) +- Fixes #6740 ([#6740](https://github.com/MetaMask/metamask-extension/pull/6740)) +- Fixes #6741 ([#6741](https://github.com/MetaMask/metamask-extension/pull/6741)) +- Fixes #6760, correct PropTypes for nextRoute ([#6761](https://github.com/MetaMask/metamask-extension/pull/6761)) +- Use inline source maps in development ([#6754](https://github.com/MetaMask/metamask-extension/pull/6754)) +- Document hotfix protocol ([#6589](https://github.com/MetaMask/metamask-extension/pull/6589)) +- Add codeowner for package-lock-old.json package-lock.json package.json packagelock-old.json files ([#6738](https://github.com/MetaMask/metamask-extension/pull/6738)) +- Add loading view to notification.html ([#6648](https://github.com/MetaMask/metamask-extension/pull/6648)) +- Add brave as a platform type for MetaMask ([#6731](https://github.com/MetaMask/metamask-extension/pull/6731)) ## [6.7.1] - 2019-07-28 ### Uncategorized -- [#6764](https://github.com/MetaMask/metamask-extension/pull/6764): Fix display of token amount on confirm transaction screen +- Fix display of token amount on confirm transaction screen ([#6764](https://github.com/MetaMask/metamask-extension/pull/6764)) ## [6.7.0] - 2019-07-26 ### Uncategorized -- [#6623](https://github.com/MetaMask/metamask-extension/pull/6623): Improve contract method data fetching (#6623) -- [#6551](https://github.com/MetaMask/metamask-extension/pull/6551): Adds 4byte registry fallback to getMethodData() (#6435) -- [#6718](https://github.com/MetaMask/metamask-extension/pull/6718): Add delete to custom RPC form -- [#6700](https://github.com/MetaMask/metamask-extension/pull/6700): Fix styles on 'import account' page, update help link -- [#6714](https://github.com/MetaMask/metamask-extension/pull/6714): Wrap smaller custom block explorer url text -- [#6706](https://github.com/MetaMask/metamask-extension/pull/6706): Pin ethereumjs-tx -- [#6700](https://github.com/MetaMask/metamask-extension/pull/6700): Fix styles on 'import account' page, update help link -- [#6775](https://github.com/MetaMask/metamask-extension/pull/6775): Started adding visual documentation of MetaMask plugin components with the account menu component first +- Improve contract method data fetching ([#6623](https://github.com/MetaMask/metamask-extension/pull/6623)) +- Adds 4byte registry fallback to getMethodData() ([#6551](https://github.com/MetaMask/metamask-extension/pull/6551)) +- Add delete to custom RPC form ([#6718](https://github.com/MetaMask/metamask-extension/pull/6718)) +- Fix styles on 'import account' page, update help link ([#6700](https://github.com/MetaMask/metamask-extension/pull/6700)) +- Wrap smaller custom block explorer url text ([#6714](https://github.com/MetaMask/metamask-extension/pull/6714)) +- Pin ethereumjs-tx ([#6706](https://github.com/MetaMask/metamask-extension/pull/6706)) +- Fix styles on 'import account' page, update help link ([#6700](https://github.com/MetaMask/metamask-extension/pull/6700)) +- Started adding visual documentation of MetaMask plugin components with the account menu component first ([#6775](https://github.com/MetaMask/metamask-extension/pull/6775)) ## [6.6.2] - 2019-07-17 ### Uncategorized -- [#6690](https://github.com/MetaMask/metamask-extension/pull/6690): Update dependencies, re-enable npm audit CI job -- [#6700](https://github.com/MetaMask/metamask-extension/pull/6700): Fix styles on 'import account' page, update help link +- Update dependencies, re-enable npm audit CI job ([#6690](https://github.com/MetaMask/metamask-extension/pull/6690)) +- Fix styles on 'import account' page, update help link ([#6700](https://github.com/MetaMask/metamask-extension/pull/6700)) ## [6.6.1] - 2019-06-06 ### Uncategorized -- [#6691](https://github.com/MetaMask/metamask-extension/pull/6691): Revert "Improve ENS Address Input" to fix bugs on input field on non-main networks. +- Revert "Improve ENS Address Input" to fix bugs on input field on non-main networks. ([#6691](https://github.com/MetaMask/metamask-extension/pull/6691)) ## [6.6.0] - 2019-06-04 ### Uncategorized -- [#6659](https://github.com/MetaMask/metamask-extension/pull/6659): Enable Ledger hardware wallet support on Firefox -- [#6671](https://github.com/MetaMask/metamask-extension/pull/6671): bugfix: reject enable promise on user rejection -- [#6625](https://github.com/MetaMask/metamask-extension/pull/6625): Ensures that transactions cannot be confirmed if gas limit is below 21000. -- [#6633](https://github.com/MetaMask/metamask-extension/pull/6633): Fix grammatical error in i18n endOfFlowMessage6 +- Enable Ledger hardware wallet support on Firefox ([#6659](https://github.com/MetaMask/metamask-extension/pull/6659)) +- bugfix: reject enable promise on user rejection ([#6671](https://github.com/MetaMask/metamask-extension/pull/6671)) +- Ensures that transactions cannot be confirmed if gas limit is below 21000. ([#6625](https://github.com/MetaMask/metamask-extension/pull/6625)) +- Fix grammatical error in i18n endOfFlowMessage6 ([#6633](https://github.com/MetaMask/metamask-extension/pull/6633)) ## [6.5.3] - 2019-05-16 ### Uncategorized -- [#6619](https://github.com/MetaMask/metamask-extension/pull/6619): bugfix: show extension window if locked regardless of approval -- [#6388](https://github.com/MetaMask/metamask-extension/pull/6388): Transactions/pending - check nonce against the network and mark as dropped if not included in a block -- [#6606](https://github.com/MetaMask/metamask-extension/pull/6606): Improve ENS Address Input -- [#6615](https://github.com/MetaMask/metamask-extension/pull/6615): Adds e2e test for removing imported accounts. +- bugfix: show extension window if locked regardless of approval ([#6619](https://github.com/MetaMask/metamask-extension/pull/6619)) +- Transactions/pending - check nonce against the network and mark as dropped if not included in a block ([#6388](https://github.com/MetaMask/metamask-extension/pull/6388)) +- Improve ENS Address Input ([#6606](https://github.com/MetaMask/metamask-extension/pull/6606)) +- Adds e2e test for removing imported accounts. ([#6615](https://github.com/MetaMask/metamask-extension/pull/6615)) ## [6.5.2] - 2019-05-15 ### Uncategorized -- [#6613](https://github.com/MetaMask/metamask-extension/pull/6613): Hardware Wallet Fix +- Hardware Wallet Fix ([#6613](https://github.com/MetaMask/metamask-extension/pull/6613)) ## [6.5.1] - 2019-05-14 ### Uncategorized - Fix bug where approve method would show a warning. #6602 -- [#6593](https://github.com/MetaMask/metamask-extension/pull/6593): Fix wording of autoLogoutTimeLimitDescription +- Fix wording of autoLogoutTimeLimitDescription ([#6593](https://github.com/MetaMask/metamask-extension/pull/6593)) ## [6.5.0] - 2019-05-13 ### Uncategorized -- [#6568](https://github.com/MetaMask/metamask-extension/pull/6568): feature: integrate gaba/PhishingController -- [#6490](https://github.com/MetaMask/metamask-extension/pull/6490): Redesign custom RPC form -- [#6558](https://github.com/MetaMask/metamask-extension/pull/6558): Adds auto logout with customizable time frame -- [#6578](https://github.com/MetaMask/metamask-extension/pull/6578): Fixes ability to send to token contract addresses -- [#6557](https://github.com/MetaMask/metamask-extension/pull/6557): Adds drag and drop functionality to seed phrase entry. -- [#6526](https://github.com/MetaMask/metamask-extension/pull/6526): Include token checksum address in prices lookup for token rates -- [#6502](https://github.com/MetaMask/metamask-extension/pull/6502): Add subheader to all settings subviews -- [#6501](https://github.com/MetaMask/metamask-extension/pull/6501): Improve confirm screen loading performance by fixing home screen rendering bug +- feature: integrate gaba/PhishingController ([#6568](https://github.com/MetaMask/metamask-extension/pull/6568)) +- Redesign custom RPC form ([#6490](https://github.com/MetaMask/metamask-extension/pull/6490)) +- Adds auto logout with customizable time frame ([#6558](https://github.com/MetaMask/metamask-extension/pull/6558)) +- Fixes ability to send to token contract addresses ([#6578](https://github.com/MetaMask/metamask-extension/pull/6578)) +- Adds drag and drop functionality to seed phrase entry. ([#6557](https://github.com/MetaMask/metamask-extension/pull/6557)) +- Include token checksum address in prices lookup for token rates ([#6526](https://github.com/MetaMask/metamask-extension/pull/6526)) +- Add subheader to all settings subviews ([#6502](https://github.com/MetaMask/metamask-extension/pull/6502)) +- Improve confirm screen loading performance by fixing home screen rendering bug ([#6501](https://github.com/MetaMask/metamask-extension/pull/6501)) ## [6.4.1] - 2019-04-26 ### Uncategorized -- [#6521](https://github.com/MetaMask/metamask-extension/pull/6521): Revert "Adds 4byte registry fallback to getMethodData()" to fix stalling bug. +- Revert "Adds 4byte registry fallback to getMethodData()" to fix stalling bug. ([#6521](https://github.com/MetaMask/metamask-extension/pull/6521)) ## [6.4.0] - 2019-04-18 ### Uncategorized -- [#6445](https://github.com/MetaMask/metamask-extension/pull/6445): \* Move send to pages/ -- [#6470](https://github.com/MetaMask/metamask-extension/pull/6470): update publishing.md with dev diagram -- [#6403](https://github.com/MetaMask/metamask-extension/pull/6403): Update to eth-method-registry@1.2.0 -- [#6468](https://github.com/MetaMask/metamask-extension/pull/6468): Fix switcher height when Custom RPC is selected or loading -- [#6459](https://github.com/MetaMask/metamask-extension/pull/6459): feature: add Goerli support -- [#6444](https://github.com/MetaMask/metamask-extension/pull/6444): Fixes #6321 & #6421 - Add Localhost 8545 for network dropdown names -- [#6454](https://github.com/MetaMask/metamask-extension/pull/6454): Bump eth-contract-metadata -- [#6448](https://github.com/MetaMask/metamask-extension/pull/6448): Remove unneeded array cloning in getSendToAccounts selector -- [#6056](https://github.com/MetaMask/metamask-extension/pull/6056): repeated getSelectedAddress() func send.selectors.js removed -- [#6422](https://github.com/MetaMask/metamask-extension/pull/6422): Added Chrome limited site access solution doc -- [#6424](https://github.com/MetaMask/metamask-extension/pull/6424): feature: switch token pricing to CoinGecko API -- [#6428](https://github.com/MetaMask/metamask-extension/pull/6428): Don't inject web3 on sharefile.com -- [#6417](https://github.com/MetaMask/metamask-extension/pull/6417): Metrics updates -- [#6420](https://github.com/MetaMask/metamask-extension/pull/6420): Fix links to MetamaskInpageProvider in porting_to_new_environment.md -- [#6362](https://github.com/MetaMask/metamask-extension/pull/6362): Remove broken image walkthrough from metamaskbot comment -- [#6401](https://github.com/MetaMask/metamask-extension/pull/6401): metamask-controller - use improved provider-as-middleware utility -- [#6406](https://github.com/MetaMask/metamask-extension/pull/6406): remove user actions controller -- [#6399](https://github.com/MetaMask/metamask-extension/pull/6399): doc - publishing - typo fix -- [#6396](https://github.com/MetaMask/metamask-extension/pull/6396): pin eth-contract-metadata to last commit hash -- [#6397](https://github.com/MetaMask/metamask-extension/pull/6397): Change coinbase to wyre -- [#6395](https://github.com/MetaMask/metamask-extension/pull/6395): bump ledger and trezor keyring -- [#6389](https://github.com/MetaMask/metamask-extension/pull/6389): Fix display of gas chart on Ethereum networks -- [#6382](https://github.com/MetaMask/metamask-extension/pull/6382): Remove NoticeController +- Move send to pages/ ([#6445](https://github.com/MetaMask/metamask-extension/pull/6445)) +- update publishing.md with dev diagram ([#6470](https://github.com/MetaMask/metamask-extension/pull/6470)) +- Update to eth-method-registry@1.2.0 ([#6403](https://github.com/MetaMask/metamask-extension/pull/6403)) +- Fix switcher height when Custom RPC is selected or loading ([#6468](https://github.com/MetaMask/metamask-extension/pull/6468)) +- feature: add Goerli support ([#6459](https://github.com/MetaMask/metamask-extension/pull/6459)) +- Fixes #6321 & #6421 - Add Localhost 8545 for network dropdown names ([#6444](https://github.com/MetaMask/metamask-extension/pull/6444)) +- Bump eth-contract-metadata ([#6454](https://github.com/MetaMask/metamask-extension/pull/6454)) +- Remove unneeded array cloning in getSendToAccounts selector ([#6448](https://github.com/MetaMask/metamask-extension/pull/6448)) +- repeated getSelectedAddress() func send.selectors.js removed ([#6056](https://github.com/MetaMask/metamask-extension/pull/6056)) +- Added Chrome limited site access solution doc ([#6422](https://github.com/MetaMask/metamask-extension/pull/6422)) +- feature: switch token pricing to CoinGecko API ([#6424](https://github.com/MetaMask/metamask-extension/pull/6424)) +- Don't inject web3 on sharefile.com ([#6428](https://github.com/MetaMask/metamask-extension/pull/6428)) +- Metrics updates ([#6417](https://github.com/MetaMask/metamask-extension/pull/6417)) +- Fix links to MetamaskInpageProvider in porting_to_new_environment.md ([#6420](https://github.com/MetaMask/metamask-extension/pull/6420)) +- Remove broken image walkthrough from metamaskbot comment ([#6362](https://github.com/MetaMask/metamask-extension/pull/6362)) +- metamask-controller - use improved provider-as-middleware utility ([#6401](https://github.com/MetaMask/metamask-extension/pull/6401)) +- remove user actions controller ([#6406](https://github.com/MetaMask/metamask-extension/pull/6406)) +- doc - publishing - typo fix ([#6399](https://github.com/MetaMask/metamask-extension/pull/6399)) +- pin eth-contract-metadata to last commit hash ([#6396](https://github.com/MetaMask/metamask-extension/pull/6396)) +- Change coinbase to wyre ([#6397](https://github.com/MetaMask/metamask-extension/pull/6397)) +- bump ledger and trezor keyring ([#6395](https://github.com/MetaMask/metamask-extension/pull/6395)) +- Fix display of gas chart on Ethereum networks ([#6389](https://github.com/MetaMask/metamask-extension/pull/6389)) +- Remove NoticeController ([#6382](https://github.com/MetaMask/metamask-extension/pull/6382)) ## [6.3.2] - 2019-04-08 ### Uncategorized -- [#6389](https://github.com/MetaMask/metamask-extension/pull/6389): Fix display of gas chart on ethereum networks -- [#6395](https://github.com/MetaMask/metamask-extension/pull/6395): Fixes for signing methods for ledger and trezor devices -- [#6397](https://github.com/MetaMask/metamask-extension/pull/6397): Fix Wyre link +- Fix display of gas chart on ethereum networks ([#6389](https://github.com/MetaMask/metamask-extension/pull/6389)) +- Fixes for signing methods for ledger and trezor devices ([#6395](https://github.com/MetaMask/metamask-extension/pull/6395)) +- Fix Wyre link ([#6397](https://github.com/MetaMask/metamask-extension/pull/6397)) ## [6.3.1] - 2019-03-29 ### Uncategorized -- [#6353](https://github.com/MetaMask/metamask-extension/pull/6353): Open restore vault in full screen when clicked from popup -- [#6372](https://github.com/MetaMask/metamask-extension/pull/6372): Prevents duplicates of account addresses from showing in send screen "To" dropdown -- [#6374](https://github.com/MetaMask/metamask-extension/pull/6374): Ensures users are placed on correct confirm screens even when registry service fails +- Open restore vault in full screen when clicked from popup ([#6353](https://github.com/MetaMask/metamask-extension/pull/6353)) +- Prevents duplicates of account addresses from showing in send screen "To" dropdown ([#6372](https://github.com/MetaMask/metamask-extension/pull/6372)) +- Ensures users are placed on correct confirm screens even when registry service fails ([#6374](https://github.com/MetaMask/metamask-extension/pull/6374)) ## [6.3.0] - 2019-03-26 ### Uncategorized -- [#6300](https://github.com/MetaMask/metamask-extension/pull/6300): Gas chart hidden on custom networks -- [#6301](https://github.com/MetaMask/metamask-extension/pull/6301): Fix gas fee in the submitted step of the transaction details activity log -- [#6302](https://github.com/MetaMask/metamask-extension/pull/6302): Replaces the coinbase link in the deposit modal with one for wyre -- [#6307](https://github.com/MetaMask/metamask-extension/pull/6307): Centre the notification in the current window -- [#6312](https://github.com/MetaMask/metamask-extension/pull/6312): Fixes popups not showing when screen size is odd -- [#6326](https://github.com/MetaMask/metamask-extension/pull/6326): Fix oversized loading overlay on gas customization modal. -- [#6330](https://github.com/MetaMask/metamask-extension/pull/6330): Stop reloading dapps on network change allowing dapps to decide if it should refresh or not -- [#6332](https://github.com/MetaMask/metamask-extension/pull/6332): Enable mobile sync -- [#6333](https://github.com/MetaMask/metamask-extension/pull/6333): Redesign of the settings screen -- [#6340](https://github.com/MetaMask/metamask-extension/pull/6340): Cancel transactions and signature requests on the closing of notification windows -- [#6341](https://github.com/MetaMask/metamask-extension/pull/6341): Disable transaction "Cancel" button when balance is insufficient -- [#6347](https://github.com/MetaMask/metamask-extension/pull/6347): Enable privacy mode by default for first time users +- Gas chart hidden on custom networks ([#6300](https://github.com/MetaMask/metamask-extension/pull/6300)) +- Fix gas fee in the submitted step of the transaction details activity log ([#6301](https://github.com/MetaMask/metamask-extension/pull/6301)) +- Replaces the coinbase link in the deposit modal with one for wyre ([#6302](https://github.com/MetaMask/metamask-extension/pull/6302)) +- Centre the notification in the current window ([#6307](https://github.com/MetaMask/metamask-extension/pull/6307)) +- Fixes popups not showing when screen size is odd ([#6312](https://github.com/MetaMask/metamask-extension/pull/6312)) +- Fix oversized loading overlay on gas customization modal. ([#6326](https://github.com/MetaMask/metamask-extension/pull/6326)) +- Stop reloading dapps on network change allowing dapps to decide if it should refresh or not ([#6330](https://github.com/MetaMask/metamask-extension/pull/6330)) +- Enable mobile sync ([#6332](https://github.com/MetaMask/metamask-extension/pull/6332)) +- Redesign of the settings screen ([#6333](https://github.com/MetaMask/metamask-extension/pull/6333)) +- Cancel transactions and signature requests on the closing of notification windows ([#6340](https://github.com/MetaMask/metamask-extension/pull/6340)) +- Disable transaction "Cancel" button when balance is insufficient ([#6341](https://github.com/MetaMask/metamask-extension/pull/6341)) +- Enable privacy mode by default for first time users ([#6347](https://github.com/MetaMask/metamask-extension/pull/6347)) ## [6.2.2] - 2019-03-12 ### Uncategorized -- [#6271](https://github.com/MetaMask/metamask-extension/pull/6271): Centre all notification popups -- [#6268](https://github.com/MetaMask/metamask-extension/pull/6268): Improve Korean translations -- [#6279](https://github.com/MetaMask/metamask-extension/pull/6279): Nonmultiple notifications for batch txs -- [#6280](https://github.com/MetaMask/metamask-extension/pull/6280): No longer check network when validating checksum addresses +- Centre all notification popups ([#6271](https://github.com/MetaMask/metamask-extension/pull/6271)) +- Improve Korean translations ([#6268](https://github.com/MetaMask/metamask-extension/pull/6268)) +- Nonmultiple notifications for batch txs ([#6279](https://github.com/MetaMask/metamask-extension/pull/6279)) +- No longer check network when validating checksum addresses ([#6280](https://github.com/MetaMask/metamask-extension/pull/6280)) ## [6.2.1] - 2019-03-11 ## [6.2.0] - 2019-03-05 ### Uncategorized -- [#6192](https://github.com/MetaMask/metamask-extension/pull/6192): Improves design and UX of onboarding flow -- [#6195](https://github.com/MetaMask/metamask-extension/pull/6195): Fixes gas estimation when sending to contracts -- [#6223](https://github.com/MetaMask/metamask-extension/pull/6223): Fixes display of notification windows when metamask is active in a tab -- [#6171](https://github.com/MetaMask/metamask-extension/pull/6171): Adds MetaMetrics usage analytics system +- Improves design and UX of onboarding flow ([#6192](https://github.com/MetaMask/metamask-extension/pull/6192)) +- Fixes gas estimation when sending to contracts ([#6195](https://github.com/MetaMask/metamask-extension/pull/6195)) +- Fixes display of notification windows when metamask is active in a tab ([#6223](https://github.com/MetaMask/metamask-extension/pull/6223)) +- Adds MetaMetrics usage analytics system ([#6171](https://github.com/MetaMask/metamask-extension/pull/6171)) ## [6.1.0] - 2019-02-20 ### Uncategorized -- [#6182](https://github.com/MetaMask/metamask-extension/pull/6182): Change "Token Address" to "Token Contract Address" -- [#6177](https://github.com/MetaMask/metamask-extension/pull/6177): Fixes #6176 -- [#6146](https://github.com/MetaMask/metamask-extension/pull/6146): \* Add Copy Tx ID button to transaction-list-item-details -- [#6133](https://github.com/MetaMask/metamask-extension/pull/6133): Checksum address before slicing it for the confirm screen -- [#6147](https://github.com/MetaMask/metamask-extension/pull/6147): Add button to force edit token symbol when adding custom token -- [#6124](https://github.com/MetaMask/metamask-extension/pull/6124): recent-blocks - dont listen for block when on infura providers -[#5973] (https://github.com/MetaMask/metamask-extension/pull/5973): Fix incorrectly showing checksums on non-ETH blockchains (issue 5838) +- Change "Token Address" to "Token Contract Address" ([#6182](https://github.com/MetaMask/metamask-extension/pull/6182)) +- Fixes #6176 ([#6177](https://github.com/MetaMask/metamask-extension/pull/6177)) +- Add Copy Tx ID button to transaction-list-item-details ([#6146](https://github.com/MetaMask/metamask-extension/pull/6146)) +- Checksum address before slicing it for the confirm screen ([#6133](https://github.com/MetaMask/metamask-extension/pull/6133)) +- Add button to force edit token symbol when adding custom token ([#6147](https://github.com/MetaMask/metamask-extension/pull/6147)) +- Fix incorrectly showing checksums on non-ETH blockchains ([#6124](https://github.com/MetaMask/metamask-extension/pull/6124): recent-blocks - dont listen for block when on infura providers -[#5973] (https://github.com/MetaMask/metamask-extension/pull/5973)) ## [6.0.1] - 2019-02-12 ### Uncategorized -- [#6139](https://github.com/MetaMask/metamask-extension/pull/6139) Fix advanced gas controls on the confirm screen -- [#6134](https://github.com/MetaMask/metamask-extension/pull/6134) Trim whitespace from seed phrase during import -- [#6119](https://github.com/MetaMask/metamask-extension/pull/6119) Update Italian translation -- [#6125](https://github.com/MetaMask/metamask-extension/pull/6125) Improved Traditional Chinese translation +- Fix advanced gas controls on the confirm screen ([#6139](https://github.com/MetaMask/metamask-extension/pull/6139)) +- Trim whitespace from seed phrase during import ([#6134](https://github.com/MetaMask/metamask-extension/pull/6134)) +- Update Italian translation ([#6119](https://github.com/MetaMask/metamask-extension/pull/6119)) +- Improved Traditional Chinese translation ([#6125](https://github.com/MetaMask/metamask-extension/pull/6125)) ## [6.0.0] - 2019-02-11 ### Uncategorized -- [#6082](https://github.com/MetaMask/metamask-extension/pull/6082): Migrate all users to the new UI -- [#6114](https://github.com/MetaMask/metamask-extension/pull/6114): Add setting for inputting gas price with a text field for advanced users. -- [#6091](https://github.com/MetaMask/metamask-extension/pull/6091): Add Swap feature to CurrencyInput -- [#6090](https://github.com/MetaMask/metamask-extension/pull/6090): Change gas labels to Slow/Average/Fast -- [#6112](https://github.com/MetaMask/metamask-extension/pull/6112): Extract advanced gas input controls to their own component -- [#5929](https://github.com/MetaMask/metamask-extension/pull/5929): Update design of phishing warning screen -- [#6120](https://github.com/MetaMask/metamask-extension/pull/6120): Add class to sign footer button -- [#6116](https://github.com/MetaMask/metamask-extension/pull/6116): Fix locale codes contains underscore never being preferred +- Migrate all users to the new UI ([#6082](https://github.com/MetaMask/metamask-extension/pull/6082)) +- Add setting for inputting gas price with a text field for advanced users. ([#6114](https://github.com/MetaMask/metamask-extension/pull/6114)) +- Add Swap feature to CurrencyInput ([#6091](https://github.com/MetaMask/metamask-extension/pull/6091)) +- Change gas labels to Slow/Average/Fast ([#6090](https://github.com/MetaMask/metamask-extension/pull/6090)) +- Extract advanced gas input controls to their own component ([#6112](https://github.com/MetaMask/metamask-extension/pull/6112)) +- Update design of phishing warning screen ([#5929](https://github.com/MetaMask/metamask-extension/pull/5929)) +- Add class to sign footer button ([#6120](https://github.com/MetaMask/metamask-extension/pull/6120)) +- Fix locale codes contains underscore never being preferred ([#6116](https://github.com/MetaMask/metamask-extension/pull/6116)) ## [5.3.5] - 2019-02-04 ### Uncategorized -- [#6084](https://github.com/MetaMask/metamask-extension/pull/6087): Privacy mode fixes +- Privacy mode fixes ([#6084](https://github.com/MetaMask/metamask-extension/pull/6087)) ## [5.3.4] - 2019-01-31 ### Uncategorized -- [#6079](https://github.com/MetaMask/metamask-extension/pull/6079): fix - migration 30 +- fix - migration 30 ([#6079](https://github.com/MetaMask/metamask-extension/pull/6079)) ## [5.3.3] - 2019-01-30 ### Uncategorized -- [#6006](https://github.com/MetaMask/metamask-extension/pull/6006): Update privacy notice -- [#6072](https://github.com/MetaMask/metamask-extension/pull/6072): Improved Spanish translations -- [#5854](https://github.com/MetaMask/metamask-extension/pull/5854): Add visual indicator when displaying a cached balance. -- [#6044](https://github.com/MetaMask/metamask-extension/pull/6044): Fix bug that interferred with using multiple custom networks. +- Update privacy notice ([#6006](https://github.com/MetaMask/metamask-extension/pull/6006)) +- Improved Spanish translations ([#6072](https://github.com/MetaMask/metamask-extension/pull/6072)) +- Add visual indicator when displaying a cached balance. ([#5854](https://github.com/MetaMask/metamask-extension/pull/5854)) +- Fix bug that interferred with using multiple custom networks. ([#6044](https://github.com/MetaMask/metamask-extension/pull/6044)) ## [5.3.2] - 2019-01-28 ### Uncategorized -- [#6021](https://github.com/MetaMask/metamask-extension/pull/6021): Order shapeshift transactions by time within the transactions list -- [#6052](https://github.com/MetaMask/metamask-extension/pull/6052): Add and use cached method signatures to reduce provider requests -- [#6048](https://github.com/MetaMask/metamask-extension/pull/6048): Refactor BalanceComponent to jsx -- [#6026](https://github.com/MetaMask/metamask-extension/pull/6026): Prevent invalid chainIds when adding custom rpcs -- [#6029](https://github.com/MetaMask/metamask-extension/pull/6029): Fix grammar error in Current Conversion -- [#6024](https://github.com/MetaMask/metamask-extension/pull/6024): Disable account dropdown on signing screens +- Order shapeshift transactions by time within the transactions list ([#6021](https://github.com/MetaMask/metamask-extension/pull/6021)) +- Add and use cached method signatures to reduce provider requests ([#6052](https://github.com/MetaMask/metamask-extension/pull/6052)) +- Refactor BalanceComponent to jsx ([#6048](https://github.com/MetaMask/metamask-extension/pull/6048)) +- Prevent invalid chainIds when adding custom rpcs ([#6026](https://github.com/MetaMask/metamask-extension/pull/6026)) +- Fix grammar error in Current Conversion ([#6029](https://github.com/MetaMask/metamask-extension/pull/6029)) +- Disable account dropdown on signing screens ([#6024](https://github.com/MetaMask/metamask-extension/pull/6024)) ## [5.3.1] - 2019-01-16 ### Uncategorized -- [#5966](https://github.com/MetaMask/metamask-extension/pull/5966): Update Slovenian translation -- [#6005](https://github.com/MetaMask/metamask-extension/pull/6005): Set auto conversion off for token/eth conversion -- [#6008](https://github.com/MetaMask/metamask-extension/pull/6008): Fix confirm screen for sending ether tx with hex data -- [#5999](https://github.com/MetaMask/metamask-extension/pull/5999): Refine app description -- [#5997](https://github.com/MetaMask/metamask-extension/pull/5997): Harden Drizzle test runner script -- [#5995](https://github.com/MetaMask/metamask-extension/pull/5995): Fix bug where MetaMask user calls non-standard ERC20 methods such as `mint`, `tokenData` will be `undefined` and an uncaught error will break the UI -- [#5970](https://github.com/MetaMask/metamask-extension/pull/5970): Fixed a word in french translation (several occurrences of connection instead of connexion) -- [#5977](https://github.com/MetaMask/metamask-extension/pull/5977): Fix Component#componentDidUpdate usage -- [#5992](https://github.com/MetaMask/metamask-extension/pull/5992): Add scrolling button to account list -- [#5989](https://github.com/MetaMask/metamask-extension/pull/5989): fix typo in phishing.html title +- Update Slovenian translation ([#5966](https://github.com/MetaMask/metamask-extension/pull/5966)) +- Set auto conversion off for token/eth conversion ([#6005](https://github.com/MetaMask/metamask-extension/pull/6005)) +- Fix confirm screen for sending ether tx with hex data ([#6008](https://github.com/MetaMask/metamask-extension/pull/6008)) +- Refine app description ([#5999](https://github.com/MetaMask/metamask-extension/pull/5999)) +- Harden Drizzle test runner script ([#5997](https://github.com/MetaMask/metamask-extension/pull/5997)) +- Fix bug where MetaMask user calls non-standard ERC20 methods such as `mint`, `tokenData` will be `undefined` and an uncaught error will break the UI ([#5995](https://github.com/MetaMask/metamask-extension/pull/5995)) +- Fixed a word in french translation ([#5970](https://github.com/MetaMask/metamask-extension/pull/5970)) +- Fix Component#componentDidUpdate usage ([#5977](https://github.com/MetaMask/metamask-extension/pull/5977)) +- Add scrolling button to account list ([#5992](https://github.com/MetaMask/metamask-extension/pull/5992)) +- fix typo in phishing.html title ([#5989](https://github.com/MetaMask/metamask-extension/pull/5989)) ## [5.3.0] - 2019-01-02 ### Uncategorized -- [#5978](https://github.com/MetaMask/metamask-extension/pull/5978): Fix etherscan links on notifications -- [#5980](https://github.com/MetaMask/metamask-extension/pull/5980): Fix drizzle tests -- [#5922](https://github.com/MetaMask/metamask-extension/pull/5922): Prevent users from changing the From field in the send screen -- [#5932](https://github.com/MetaMask/metamask-extension/pull/5932): Fix displayed time and date in the activity log. Remove vreme library, add luxon library. -- [#5924](https://github.com/MetaMask/metamask-extension/pull/5924): transactions - throw an error if a transaction is generated while the network is loading -- [#5893](https://github.com/MetaMask/metamask-extension/pull/5893): Add loading network screen +- Fix etherscan links on notifications ([#5978](https://github.com/MetaMask/metamask-extension/pull/5978)) +- Fix drizzle tests ([#5980](https://github.com/MetaMask/metamask-extension/pull/5980)) +- Prevent users from changing the From field in the send screen ([#5922](https://github.com/MetaMask/metamask-extension/pull/5922)) +- Fix displayed time and date in the activity log. Remove vreme library, add luxon library. ([#5932](https://github.com/MetaMask/metamask-extension/pull/5932)) +- transactions - throw an error if a transaction is generated while the network is loading ([#5924](https://github.com/MetaMask/metamask-extension/pull/5924)) +- Add loading network screen ([#5893](https://github.com/MetaMask/metamask-extension/pull/5893)) ## [5.2.2] - 2018-12-13 ### Uncategorized -- [#5925](https://github.com/MetaMask/metamask-extension/pull/5925): Fix speed up button not showing for transactions with the lowest nonce -- [#5923](https://github.com/MetaMask/metamask-extension/pull/5923): Update the Phishing Warning notice text to not use inline URLs -- [#5919](https://github.com/MetaMask/metamask-extension/pull/5919): Fix some styling and translations in the gas customization modal +- Fix speed up button not showing for transactions with the lowest nonce ([#5925](https://github.com/MetaMask/metamask-extension/pull/5925)) +- Update the Phishing Warning notice text to not use inline URLs ([#5923](https://github.com/MetaMask/metamask-extension/pull/5923)) +- Fix some styling and translations in the gas customization modal ([#5919](https://github.com/MetaMask/metamask-extension/pull/5919)) ## [5.2.1] - 2018-12-12 ### Uncategorized -- [#5917] bugfix: Ensures that advanced tab gas limit reflects tx gas limit +- bugfix: Ensures that advanced tab gas limit reflects tx gas limit ([#5917](https://github.com/MetaMask/metamask-extension/pull/5917)) ## [5.2.0] - 2018-12-11 ### Uncategorized -- [#5704] Implements new gas customization features for sending, confirming and speeding up transactions -- [#5886] Groups transactions - speed up, cancel and original - by nonce in the transaction history list -- [#5892] bugfix: eliminates infinite spinner issues caused by switching quickly from a loading network that ultimately fails to resolve -- [$5902] bugfix: provider crashes caused caching issues in `json-rpc-engine`. Fixed in (https://github.com/MetaMask/json-rpc-engine/commit/6de511afbd03ccef4550ea43ff4010b7d7a84039) +- Implements new gas customization features for sending, confirming and speeding up transactions ([#5704](https://github.com/MetaMask/metamask-extension/pull/5704)) +- Groups transactions - speed up, cancel and original - by nonce in the transaction history list ([#5886](https://github.com/MetaMask/metamask-extension/pull/5886)) +- bugfix: eliminates infinite spinner issues caused by switching quickly from a loading network that ultimately fails to resolve ([#5892](https://github.com/MetaMask/metamask-extension/pull/5892)) +- bugfix: provider crashes caused caching issues in `json-rpc-engine`. ([#5902](https://github.com/MetaMask/metamask-extension/pull/5902)) + - Fixed in (https://github.com/MetaMask/json-rpc-engine/commit/6de511afbd03ccef4550ea43ff4010b7d7a84039) ## [5.1.0] - 2018-12-03 ### Uncategorized -- [#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. +- Fixed an infinite spinner bug. ([#5860](https://github.com/MetaMask/metamask-extension/pull/5860)) +- Update phishing warning copy ([#5875](https://github.com/MetaMask/metamask-extension/pull/5875)) +- bugfix: normalize contract addresss when fetching exchange rates ([#5863](https://github.com/MetaMask/metamask-extension/pull/5863)) +- Use selector for state.metamask.accounts in all cases. ([#5843](https://github.com/MetaMask/metamask-extension/pull/5843)) ## [5.0.4] - 2018-11-29 ### Uncategorized -- [#5878](https://github.com/MetaMask/metamask-extension/pull/5878): Formats 32-length byte strings passed to personal_sign as hex, rather than UTF8. -- [#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 +- Formats 32-length byte strings passed to personal_sign as hex, rather than UTF8. ([#5878](https://github.com/MetaMask/metamask-extension/pull/5878)) +- transactions/tx-gas-utils - add the acctual response for eth_getCode for NO_CONTRACT_ERROR's && add a debug object to simulationFailed ([#5840](https://github.com/MetaMask/metamask-extension/pull/5840)) +- Soften accusatory language on phishing warning ([#5848](https://github.com/MetaMask/metamask-extension/pull/5848)) +- Open full-screen UI on install ([#5835](https://github.com/MetaMask/metamask-extension/pull/5835)) - 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 +- Hide app-header when provider request pending ([#5831](https://github.com/MetaMask/metamask-extension/pull/5831)) +- transactions - autofill gasPrice for retry attempts with either the recomened gasprice or a %10 bump ([#5786](https://github.com/MetaMask/metamask-extension/pull/5786)) +- transactions - ensure err is defined when setting tx failed ([#5801](https://github.com/MetaMask/metamask-extension/pull/5801)) +- Consider HW Wallets for signTypedMessage ([#5792](https://github.com/MetaMask/metamask-extension/pull/5792)) +- Show disabled cursor in .network-disabled state ([#5829](https://github.com/MetaMask/metamask-extension/pull/5829)) +- Trim whitespace from seed phrase during import ([#5827](https://github.com/MetaMask/metamask-extension/pull/5827)) +- Show Connect Requests count in extension badge ([#5832](https://github.com/MetaMask/metamask-extension/pull/5832)) +- Increase Token Symbol length to twelve ([#5816](https://github.com/MetaMask/metamask-extension/pull/5816)) +- 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. ([#5819](https://github.com/MetaMask/metamask-extension/pull/5819)) +- Bump Node version to 8.13 ([#5810](https://github.com/MetaMask/metamask-extension/pull/5810)) +- Add Firefox and Brave support for Trezor ([#5797](https://github.com/MetaMask/metamask-extension/pull/5797)) +- Fix usage of setState in ConfirmTransactionBase#handleSubmit ([#5799](https://github.com/MetaMask/metamask-extension/pull/5799)) +- Show byte count for hex data on confirm screen ([#5798](https://github.com/MetaMask/metamask-extension/pull/5798)) +- Default to the new UI for first time users ([#5334](https://github.com/MetaMask/metamask-extension/pull/5334)) +- Bump eth-ledger-bridge-keyring ([#5791](https://github.com/MetaMask/metamask-extension/pull/5791)) ## [5.0.3] - 2018-11-20 ### Uncategorized -- [#5547](https://github.com/MetaMask/metamask-extension/pull/5547): Bundle some ui dependencies separately to limit the build size of ui.js +- Bundle some ui dependencies separately to limit the build size of ui.js ([#5547](https://github.com/MetaMask/metamask-extension/pull/5547)) - Resubmit approved transactions on new block, to fix bug where an error can stick transactions in this state. - Fixed a bug that could cause an error when sending the max number of tokens. @@ -1215,9 +1220,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Fix bug where data lookups like balances would get stale data (stopped block-tracker bug) - Transaction Details now show entry for onchain failure -- [#5559](https://github.com/MetaMask/metamask-extension/pull/5559) Localize language names in translation select list -- [#5283](https://github.com/MetaMask/metamask-extension/pull/5283): Fix bug when eth.getCode() called with no contract -- [#5563](https://github.com/MetaMask/metamask-extension/pull/5563#pullrequestreview-166769174) Feature: improve Hatian Creole translations +- Localize language names in translation select list ([#5559](https://github.com/MetaMask/metamask-extension/pull/5559)) +- Fix bug when eth.getCode() called with no contract ([#5283](https://github.com/MetaMask/metamask-extension/pull/5283)) +- Feature: improve Hatian Creole translations ([#5563](https://github.com/MetaMask/metamask-extension/pull/5563#pullrequestreview-166769174)) - Feature: improve Slovenian translations - Add support for alternate `wallet_watchAsset` rpc method name - Attempt chain ID lookup via `eth_chainId` before `net_version` @@ -1242,9 +1247,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [4.14.0] - 2018-10-11 ### Uncategorized - Update transaction statuses when switching networks. -- [#5470](https://github.com/MetaMask/metamask-extension/pull/5470) 100% coverage in French locale, fixed the procedure to verify proposed locale. +- 100% coverage in French locale, fixed the procedure to verify proposed locale. ([#5470](https://github.com/MetaMask/metamask-extension/pull/5470)) - Added rudimentary support for the subscription API to support web3 1.0 and Truffle's Drizzle. -- [#5502](https://github.com/MetaMask/metamask-extension/pull/5502) Update Italian translation. +- Update Italian translation. ([#5502](https://github.com/MetaMask/metamask-extension/pull/5502)) ## [4.13.0] - 2018-10-04 ### Uncategorized @@ -1264,61 +1269,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [4.10.0] - 2018-09-18 ### Uncategorized -- [#4803](https://github.com/MetaMask/metamask-extension/pull/4803): Implement EIP-712: Sign typed data, but continue to support v1. -- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. -- [#4279](https://github.com/MetaMask/metamask-extension/pull/4279): New BlockTracker and Json-Rpc-Engine based Provider. -- [#5050](https://github.com/MetaMask/metamask-extension/pull/5050): Add Ledger hardware wallet support. -- [#4919](https://github.com/MetaMask/metamask-extension/pull/4919): Refactor and Redesign Transaction List. -- [#5182](https://github.com/MetaMask/metamask-extension/pull/5182): Add Transaction Details to the Transaction List view. -- [#5229](https://github.com/MetaMask/metamask-extension/pull/5229): Clear old seed words when importing new seed words. -- [#5264](https://github.com/MetaMask/metamask-extension/pull/5264): Improve click area for adjustment arrows buttons. -- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method. -- [#5189](https://github.com/MetaMask/metamask-extension/pull/5189): Fix bug where Ropsten loading message is shown when connecting to Kovan. -- [#5256](https://github.com/MetaMask/metamask-extension/pull/5256): Add mock EIP-1102 support +- Implement EIP-712: Sign typed data, but continue to support v1. ([#4803](https://github.com/MetaMask/metamask-extension/pull/4803)) +- Restore multiple consecutive accounts with balances. ([#4898](https://github.com/MetaMask/metamask-extension/pull/4898)) +- New BlockTracker and Json-Rpc-Engine based Provider. ([#4279](https://github.com/MetaMask/metamask-extension/pull/4279)) +- Add Ledger hardware wallet support. ([#5050](https://github.com/MetaMask/metamask-extension/pull/5050)) +- Refactor and Redesign Transaction List. ([#4919](https://github.com/MetaMask/metamask-extension/pull/4919)) +- Add Transaction Details to the Transaction List view. ([#5182](https://github.com/MetaMask/metamask-extension/pull/5182)) +- Clear old seed words when importing new seed words. ([#5229](https://github.com/MetaMask/metamask-extension/pull/5229)) +- Improve click area for adjustment arrows buttons. ([#5264](https://github.com/MetaMask/metamask-extension/pull/5264)) +- Add new metamask_watchAsset method. ([#4606](https://github.com/MetaMask/metamask-extension/pull/4606)) +- Fix bug where Ropsten loading message is shown when connecting to Kovan. ([#5189](https://github.com/MetaMask/metamask-extension/pull/5189)) +- Add mock EIP-1102 support ([#5256](https://github.com/MetaMask/metamask-extension/pull/5256)) ## [4.9.3] - 2018-08-16 ### Uncategorized -- [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses. -- [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link. -- [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly. +- QR code scan for recipient addresses. ([#4897](https://github.com/MetaMask/metamask-extension/pull/4897)) +- Add a download seed phrase link. ([#4961](https://github.com/MetaMask/metamask-extension/pull/4961)) +- Fix bug where gas was not updating properly. ([#5060](https://github.com/MetaMask/metamask-extension/pull/5060)) ## [4.9.2] - 2018-08-10 ### Uncategorized -- [#5020](https://github.com/MetaMask/metamask-extension/pull/5020): Fix bug in migration #28 ( moving tokens to specific accounts ) +- Fix bug in migration #28 ([#5020](https://github.com/MetaMask/metamask-extension/pull/5020)) ## [4.9.1] - 2018-08-09 ### Uncategorized -- [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network. -- [#4989](https://github.com/MetaMask/metamask-extension/pull/4989): Continue to use original signedTypedData. -- [#5010](https://github.com/MetaMask/metamask-extension/pull/5010): Fix ENS resolution issues. -- [#5000](https://github.com/MetaMask/metamask-extension/pull/5000): Show error while allowing confirmation of tx where simulation fails. -- [#4995](https://github.com/MetaMask/metamask-extension/pull/4995): Shows retry button on dApp initialized transactions. +- Allow to have tokens per account and network. ([#4884](https://github.com/MetaMask/metamask-extension/pull/4884)) +- Continue to use original signedTypedData. ([#4989](https://github.com/MetaMask/metamask-extension/pull/4989)) +- Fix ENS resolution issues. ([#5010](https://github.com/MetaMask/metamask-extension/pull/5010)) +- Show error while allowing confirmation of tx where simulation fails. ([#5000](https://github.com/MetaMask/metamask-extension/pull/5000)) +- Shows retry button on dApp initialized transactions. ([#4995](https://github.com/MetaMask/metamask-extension/pull/4995)) ## [4.9.0] - 2018-08-07 ### Uncategorized -- [#4926](https://github.com/MetaMask/metamask-extension/pull/4926): Show retry button on the latest tx of the earliest nonce. -- [#4888](https://github.com/MetaMask/metamask-extension/pull/4888): Suggest using the new user interface. -- [#4947](https://github.com/MetaMask/metamask-extension/pull/4947): Prevent sending multiple transasctions on multiple confirm clicks. -- [#4844](https://github.com/MetaMask/metamask-extension/pull/4844): Add new tokens auto detection. -- [#4667](https://github.com/MetaMask/metamask-extension/pull/4667): Remove rejected transactions from transaction history. -- [#4625](https://github.com/MetaMask/metamask-extension/pull/4625): Add Trezor Support. -- [#4625](https://github.com/MetaMask/metamask-extension/pull/4625/commits/523cf9ad33d88719520ae5e7293329d133b64d4d): Allow to remove accounts (Imported and Hardware Wallets) -- [#4814](https://github.com/MetaMask/metamask-extension/pull/4814): Add hex data input to send screen. -- [#4691](https://github.com/MetaMask/metamask-extension/pull/4691): Redesign of the Confirm Transaction Screen. -- [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed. -- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): Allow the use of HTTP prefix for custom rpc urls. -- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case. -- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. +- Show retry button on the latest tx of the earliest nonce. ([#4926](https://github.com/MetaMask/metamask-extension/pull/4926)) +- Suggest using the new user interface. ([#4888](https://github.com/MetaMask/metamask-extension/pull/4888)) +- Prevent sending multiple transasctions on multiple confirm clicks. ([#4947](https://github.com/MetaMask/metamask-extension/pull/4947)) +- Add new tokens auto detection. ([#4844](https://github.com/MetaMask/metamask-extension/pull/4844)) +- Remove rejected transactions from transaction history. ([#4667](https://github.com/MetaMask/metamask-extension/pull/4667)) +- Add Trezor Support. ([#4625](https://github.com/MetaMask/metamask-extension/pull/4625)) +- Allow to remove accounts ([#4625](https://github.com/MetaMask/metamask-extension/pull/4625/commits/523cf9ad33d88719520ae5e7293329d133b64d4d)) +- Add hex data input to send screen. ([#4814](https://github.com/MetaMask/metamask-extension/pull/4814)) +- Redesign of the Confirm Transaction Screen. ([#4691](https://github.com/MetaMask/metamask-extension/pull/4691)) +- Now shows notifications when transactions are completed. ([#4840](https://github.com/MetaMask/metamask-extension/pull/4840)) +- Allow the use of HTTP prefix for custom rpc urls. ([#4855](https://github.com/MetaMask/metamask-extension/pull/4855)) +- network.js: convert rpc protocol to lower case. ([#4855](https://github.com/MetaMask/metamask-extension/pull/4855)) +- Restore multiple consecutive accounts with balances. ([#4898](https://github.com/MetaMask/metamask-extension/pull/4898)) ## [4.8.0] - 2018-06-18 ### Uncategorized -- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error. -- [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use. -- [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers. -- [#4524](https://github.com/MetaMask/metamask-extension/pull/4524): Fix for Brave i18n getAcceptLanguages. -- [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released. -- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice. -- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan. +- Attempting to import an empty private key will now show a clear error. ([#4513](https://github.com/MetaMask/metamask-extension/pull/4513)) +- Fix bug where metamask data would stop being written to disk after prolonged use. ([#4570](https://github.com/MetaMask/metamask-extension/pull/4570)) +- Fix bug where account reset did not work with custom RPC providers. ([#4523](https://github.com/MetaMask/metamask-extension/pull/4523)) +- Fix for Brave i18n getAcceptLanguages. ([#4524](https://github.com/MetaMask/metamask-extension/pull/4524)) +- Fix bug where nonce mutex was never released. ([#4557](https://github.com/MetaMask/metamask-extension/pull/4557)) +- Add phishing notice. ([#4566](https://github.com/MetaMask/metamask-extension/pull/4566)) +- Allow Copying Token Addresses and link to Token on Etherscan. ([#4591](https://github.com/MetaMask/metamask-extension/pull/4591)) ## [4.7.4] - 2018-06-05 ### Uncategorized diff --git a/README.md b/README.md index cc546cd93..b05ac8625 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Hey! We are hiring JavaScript Engineers! [Apply here](https://boards.greenhouse. You can find the latest version of MetaMask on [our official website](https://metamask.io/). For help using MetaMask, visit our [User Support Site](https://metamask.zendesk.com/hc/en-us). -For [general questions](https://metamask.zendesk.com/hc/en-us/community/topics/360000682532-General), [feature requests](https://metamask.zendesk.com/hc/en-us/community/topics/360000682552-Feature-Requests-Ideas), or [developer questions](https://metamask.zendesk.com/hc/en-us/community/topics/360001751291-Developer-Questions), visit our [Community Forum](https://metamask.zendesk.com/hc/en-us/community/topics). +For [general questions](https://community.metamask.io/c/learn/26), [feature requests](https://community.metamask.io/c/feature-requests-ideas/13), or [developer questions](https://community.metamask.io/c/developer-questions/11), visit our [Community Forum](https://community.metamask.io/). MetaMask supports Firefox, Google Chrome, and Chromium-based browsers. We recommend using the latest available browser version. diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 5516f4465..18c3b7378 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "ማሰሺያዎት አልተደገፈም..." }, - "builtInCalifornia": { - "message": "MetaMask ካሊፎርኒያ ውስጥ ተዘጋጅቶ የተገነባ ነው።" - }, "buyWithWyre": { "message": "ETH በ Wyre ይግዙ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 14ecf04ed..90f7618c8 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "متصفحك غير مدعوم..." }, - "builtInCalifornia": { - "message": "تم تصميم وإنشاء MetaMask في ولاية كاليفورنيا." - }, "buyWithWyre": { "message": "قم بشراء عملة إيثير بواسطة Wyre" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 9903c065a..26cae225a 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Браузърът ви не се поддържа ..." }, - "builtInCalifornia": { - "message": "MetaMask е проектиран и създаден в Калифорния." - }, "buyWithWyre": { "message": "Купете ETH с Wyre" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index c35429f22..2b1724128 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "আপনার ব্রাউজার সমর্থিত নয়..." }, - "builtInCalifornia": { - "message": "MetaMask ক্যালিফোর্নিয়াতে ডিজাইন করা এবং নির্মিত।" - }, "buyWithWyre": { "message": "Wyre দিয়ে ETH ক্রয় করুন" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 1608d3fbf..7571840c8 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "El teu navegador no és suportat..." }, - "builtInCalifornia": { - "message": "MetaMask ha estat dissenyat i desenvolupat a Califòrnia." - }, "buyWithWyre": { "message": "Compra ETH amb Wyre" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index cc75f6444..3e158ca01 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -46,9 +46,6 @@ "blockiesIdenticon": { "message": "Použít Blockies Identicon" }, - "builtInCalifornia": { - "message": "MetaMask je navržen a vytvořen v Kalifornii." - }, "cancel": { "message": "Zrušit" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 05daa9b45..8138bdc93 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Din browser er ikke understøttet..." }, - "builtInCalifornia": { - "message": "MetaMask er designet og bygget i Californien." - }, "buyWithWyre": { "message": "Køb ETH med Wyre" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index e38061843..48cfd91bc 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -140,9 +140,6 @@ "browserNotSupported": { "message": "Ihr Browser wird nicht unterstützt …" }, - "builtInCalifornia": { - "message": "MetaMask wurde in Kalifornien entwickelt und gebaut." - }, "buyWithWyre": { "message": "ETH mit Wyre kaufen" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index d18a8ef43..59ba5729b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Το Πρόγραμμα Περιήγησής σας δεν υποστηρίζεται..." }, - "builtInCalifornia": { - "message": "Το MetaMask έχει σχεδιαστεί και αναπτυχθεί στην Καλιφόρνια." - }, "buyWithWyre": { "message": "Αγοράστε ETH με το Wyre" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 04326924c..e5fa724cc 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -97,9 +97,15 @@ "addTokens": { "message": "Add Tokens" }, + "addressBookIcon": { + "message": "Address book icon" + }, "advanced": { "message": "Advanced" }, + "advancedGasPriceTitle": { + "message": "Gas price" + }, "advancedOptions": { "message": "Advanced Options" }, @@ -150,6 +156,9 @@ "amount": { "message": "Amount" }, + "amountGasFee": { + "message": "Amount + Gas Fee" + }, "amountWithColon": { "message": "Amount:" }, @@ -223,7 +232,7 @@ "message": "This secret code is required to recover your wallet in case you lose your device, forget your password, have to re-install MetaMask, or want to access your wallet on another device." }, "backupApprovalNotice": { - "message": "Backup your Secret Recovery code to keep your wallet and funds secure." + "message": "Backup your Secret Recovery Phrase to keep your wallet and funds secure." }, "backupNow": { "message": "Backup now" @@ -253,15 +262,21 @@ "browserNotSupported": { "message": "Your Browser is not supported..." }, - "builContactList": { + "buildContactList": { "message": "Build your contact list" }, - "builtInCalifornia": { - "message": "MetaMask is designed and built in California." + "builtAroundTheWorld": { + "message": "MetaMask is designed and built around the world." }, "buy": { "message": "Buy" }, + "buyWithTransak": { + "message": "Buy ETH with Transak" + }, + "buyWithTransakDescription": { + "message": "Transak supports debit card and bank transfers (depending on location) in 59+ countries. ETH deposits into your MetaMask account." + }, "buyWithWyre": { "message": "Buy ETH with Wyre" }, @@ -277,6 +292,9 @@ "cancel": { "message": "Cancel" }, + "cancelPopoverTitle": { + "message": "Cancel transaction" + }, "cancellationGasFee": { "message": "Cancellation Gas Fee" }, @@ -310,6 +328,9 @@ "confirmSecretBackupPhrase": { "message": "Confirm your Secret Backup Phrase" }, + "confirmSeedPhrase": { + "message": "Confirm Seed Phrase" + }, "confirmed": { "message": "Confirmed" }, @@ -414,6 +435,9 @@ "continue": { "message": "Continue" }, + "continueToTransak": { + "message": "Continue to Transak" + }, "continueToWyre": { "message": "Continue to Wyre" }, @@ -456,6 +480,9 @@ "createAccount": { "message": "Create Account" }, + "createNewWallet": { + "message": "Create a new wallet" + }, "createPassword": { "message": "Create Password" }, @@ -492,6 +519,9 @@ "customToken": { "message": "Custom Token" }, + "data": { + "message": "Data" + }, "dataBackupFoundInfo": { "message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts, and tokens. Would you like to restore this data now?" }, @@ -599,6 +629,87 @@ "editContact": { "message": "Edit Contact" }, + "editGasEducationButtonText": { + "message": "How should I choose?" + }, + "editGasEducationHighExplanation": { + "message": "This is best for swaps or other time sensitive transactions. If a swap takes too long to process it will often fail and you may lose funds." + }, + "editGasEducationLearnMoreLinkText": { + "message": "Learn more about customizing gas." + }, + "editGasEducationLowExplanation": { + "message": "Low A lower gas fee should only be selected for transactions where processing time is less important. With a lower fee, it can be be hard to predict when (or if) your transaction with be successful." + }, + "editGasEducationMediumExplanation": { + "message": "A medium gas fee is good for sending, withdrawing or other non-time sensitive but important transactions." + }, + "editGasEducationModalIntro": { + "message": "The right gas amount to select depends on the type of transaction and how important it is." + }, + "editGasEducationModalTitle": { + "message": "How to choose?" + }, + "editGasHigh": { + "message": "High" + }, + "editGasLimitOutOfBounds": { + "message": "Gas limit must be greater than 20999 and less than 7920027" + }, + "editGasLimitTooltip": { + "message": "Gas limit is the maximum units of gas you are willing to use. Units of gas are a multiplier to “Max priority fee” and “Max fee”." + }, + "editGasLow": { + "message": "Low" + }, + "editGasMaxFeeHigh": { + "message": "Max fee is higher than necessary" + }, + "editGasMaxFeeLow": { + "message": "Max fee too low for network conditions" + }, + "editGasMaxFeeTooltip": { + "message": "The max fee is the most you’ll pay (base fee + priority fee)." + }, + "editGasMaxPriorityFeeHigh": { + "message": "Max priority fee is higher than necessary. You may pay more than needed." + }, + "editGasMaxPriorityFeeLow": { + "message": "Max priority fee too low for network conditions" + }, + "editGasMaxPriorityFeeTooltip": { + "message": "Max priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction. You’ll most often pay your max setting" + }, + "editGasMaxPriorityFeeZeroError": { + "message": "Max priority fee must be at least 1 GWEI" + }, + "editGasMedium": { + "message": "Medium" + }, + "editGasPriceTooltip": { + "message": "This network requires a “Gas price” field when submitting a transaction. Gas price is the amount you will pay pay per unit of gas." + }, + "editGasSubTextAmount": { + "message": "Max amount: $1", + "description": "$1 represents a dollar amount" + }, + "editGasSubTextFee": { + "message": "Max fee: $1", + "description": "$1 represents a dollar amount" + }, + "editGasTitle": { + "message": "Edit priority" + }, + "editGasTooLow": { + "message": "Unknown processing time" + }, + "editGasTooLowTooltip": { + "message": "Your max fee is low for current market conditions. We don’t know when (or if) your transaction will be processed." + }, + "editGasTotalBannerSubtitle": { + "message": "Up to $1 ($2)", + "display": "$1 represents a fiat value" + }, "editNonceField": { "message": "Edit Nonce" }, @@ -786,6 +897,19 @@ "functionType": { "message": "Function Type" }, + "gasDisplayAcknowledgeDappButtonText": { + "message": "Edit suggested gas fee" + }, + "gasDisplayDappWarning": { + "message": "This gas fee has been suggested by $1. Overriding this may cause a problem with your transaction. Please reach out to $1 if you have questions.", + "description": "$1 represents the Dapp's origin" + }, + "gasFee": { + "message": "Gas Fee" + }, + "gasFeeEstimate": { + "message": "Estimate" + }, "gasLimit": { "message": "Gas Limit" }, @@ -817,6 +941,26 @@ "gasPriceInfoTooltipContent": { "message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas." }, + "gasTimingMinutes": { + "message": "$1 minutes", + "description": "$1 represents a number of minutes" + }, + "gasTimingNegative": { + "message": "Maybe in $1", + "description": "$1 represents an amount of time" + }, + "gasTimingPositive": { + "message": "Likely in < $1", + "description": "$1 represents an amount of time" + }, + "gasTimingSeconds": { + "message": "$1 seconds", + "description": "$1 represents a number of seconds" + }, + "gasTimingVeryPositive": { + "message": "Very likely in < $1", + "description": "$1 represents an amount of time" + }, "gasUsed": { "message": "Gas Used" }, @@ -1070,9 +1214,19 @@ "makeAnotherSwap": { "message": "Create a new swap" }, + "makeSureNoOneWatching": { + "message": "Make sure no one is watching your screen", + "description": "Warning to users to be care while creating and saving their new seed phrase" + }, "max": { "message": "Max" }, + "maxFee": { + "message": "Max fee" + }, + "maxPriorityFee": { + "message": "Max priority fee" + }, "memo": { "message": "memo" }, @@ -1367,6 +1521,9 @@ "onlyConnectTrust": { "message": "Only connect with sites you trust." }, + "optional": { + "message": "Optional" + }, "optionalBlockExplorerUrl": { "message": "Block Explorer URL (optional)" }, @@ -1391,6 +1548,12 @@ "passwordNotLongEnough": { "message": "Password not long enough" }, + "passwordSetupDetails": { + "message": "This password will unlock your MetaMask wallet only on this device. MetaMask can not recover this password." + }, + "passwordTermsWarning": { + "message": "I understand that MetaMask cannot recover this password for me. $1" + }, "passwordsDontMatch": { "message": "Passwords Don't Match" }, @@ -1466,6 +1629,9 @@ "recipientAddressPlaceholder": { "message": "Search, public address (0x), or ENS" }, + "recommendedGasLabel": { + "message": "Recommended" + }, "recoveryPhraseReminderBackupStart": { "message": "Start here" }, @@ -1611,12 +1777,21 @@ "secretPhrase": { "message": "Enter your secret phrase here to restore your vault." }, + "secureWallet": { + "message": "Secure Wallet" + }, "securityAndPrivacy": { "message": "Security & Privacy" }, "securitySettingsDescription": { "message": "Privacy settings and wallet Secret Recovery Phrase" }, + "seedPhraseConfirm": { + "message": "Confirm Secret Recovery Phrase" + }, + "seedPhraseEnterMissingWords": { + "message": "Confirm Secret Recovery Phrase" + }, "seedPhraseIntroSidebarBulletFour": { "message": "Write down and store in multiple secret places." }, @@ -1662,6 +1837,12 @@ "seedPhraseReq": { "message": "Secret Recovery Phrases contain 12, 15, 18, 21, or 24 words" }, + "seedPhraseWriteDownDetails": { + "message": "Write down this 12-word Secret Recovery Phrase and save it in a place that you trust and only you can access." + }, + "seedPhraseWriteDownHeader": { + "message": "Write down your Secret Recovery Phrase" + }, "selectAHigherGasFee": { "message": "Select a higher gas fee to accelerate the processing of your transaction.*" }, @@ -1721,6 +1902,9 @@ "settings": { "message": "Settings" }, + "show": { + "message": "Show" + }, "showAdvancedGasInline": { "message": "Advanced gas controls" }, @@ -1784,6 +1968,15 @@ "speedUpCancellation": { "message": "Speed up this cancellation" }, + "speedUpExplanation": { + "message": "We’ve updated the gas fee based on current network conditions and have increased it by at least 10% (required by the network)." + }, + "speedUpPopoverTitle": { + "message": "Speed up transaction" + }, + "speedUpTooltipText": { + "message": "New gas fee" + }, "speedUpTransaction": { "message": "Speed up this transaction" }, @@ -2129,6 +2322,10 @@ "message": "Verified on $1 sources.", "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." }, + "swapTooManyDecimalsError": { + "message": "$1 allows up to $2 decimals", + "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" + }, "swapTransactionComplete": { "message": "Transaction complete" }, @@ -2198,6 +2395,9 @@ "symbolBetweenZeroTwelve": { "message": "Symbol must be 11 characters or fewer." }, + "syncInProgress": { + "message": "Sync in progress" + }, "syncWithMobile": { "message": "Sync with mobile" }, @@ -2289,6 +2489,28 @@ "transactionCreated": { "message": "Transaction created with a value of $1 at $2." }, + "transactionDetailDappGasHeading": { + "message": "$1 suggested gas fee", + "description": "$1 represents a dapp origin" + }, + "transactionDetailDappGasTooltip": { + "message": "This gas fee suggestion is using legacy gas estimation which may be inaccurate." + }, + "transactionDetailGasHeading": { + "message": "Estimated gas fee" + }, + "transactionDetailGasTooltipConversion": { + "message": "Learn more about gas fees" + }, + "transactionDetailGasTooltipExplanation": { + "message": "Gas fees are set by the network and fluctuate based on network traffic and transaction complexity." + }, + "transactionDetailGasTooltipIntro": { + "message": "Gas fees are paid to crypto miners who process transactions on the Ethereum network. MetaMask does not profit from gas fees." + }, + "transactionDetailGasTotalSubtitle": { + "message": "Amount + gas fee" + }, "transactionDropped": { "message": "Transaction dropped at $2." }, @@ -2304,6 +2526,15 @@ "transactionFee": { "message": "Transaction Fee" }, + "transactionHistoryBaseFee": { + "message": "Base fee (GWEI)" + }, + "transactionHistoryEffectiveGasPrice": { + "message": "Effective gas price" + }, + "transactionHistoryPriorityFee": { + "message": "Priority fee (GWEI)" + }, "transactionResubmitted": { "message": "Transaction resubmitted with gas fee increased to $1 at $2" }, @@ -2424,6 +2655,9 @@ "viewContact": { "message": "View Contact" }, + "viewFullTransactionDetails": { + "message": "View full transaction details" + }, "viewMore": { "message": "View More" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 8f41652bf..23d44c8af 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -6,7 +6,7 @@ "message": "Versión, centro de soporte técnico e información de contacto" }, "acceleratingATransaction": { - "message": "* Usar un precio de gas más alto para acelerar una transacción aumenta las posibilidades de un procesamiento más rápido en la red, pero esto no siempre se garantiza." + "message": "* Usar un precio de gas más alto para acelerar una transacción aumenta las posibilidades de un procesamiento más rápido en la red, pero esto no siempre se garantiza." }, "acceptTermsOfUse": { "message": "Leí y estoy de acuerdo con $1", @@ -52,6 +52,10 @@ "addContact": { "message": "Agregar contacto" }, + "addCustomTokenByContractAddress": { + "message": "¿No encuentra un token? Puede agregar cualquier token si copia su dirección. Puede encontrar la dirección de contrato del token en $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Esto permitirá que la red se utilice en MetaMask." }, @@ -85,7 +89,7 @@ "message": "Agregar a la libreta de direcciones" }, "addToAddressBookModalPlaceholder": { - "message": "p. ej., John D." + "message": "p. ej., John D." }, "addToken": { "message": "Agregar token" @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "El explorador no es compatible…" }, - "builContactList": { + "buildContactList": { "message": "Cree su lista de contactos" }, - "builtInCalifornia": { - "message": "MetaMask se diseñó y compiló en California." - }, "buy": { "message": "Comprar" }, @@ -268,7 +269,7 @@ "message": "Bytes" }, "canToggleInSettings": { - "message": "Puede volver a activar esta notificación desde Configuración > Alertas." + "message": "Puede volver a activar esta notificación desde Configuración -> Alertas." }, "cancel": { "message": "Cancelar" @@ -285,8 +286,11 @@ "chainIdDefinition": { "message": "El identificador de cadena que se utiliza para firmar transacciones en esta red." }, + "chainIdExistsErrorMsg": { + "message": "En este momento, la red $1 está utilizando este identificador de cadena." + }, "chromeRequiredForHardwareWallets": { - "message": "Debe usar MetaMask en Google Chrome para poder conectarse a su cartera de hardware." + "message": "Debe usar MetaMask en Google Chrome para poder conectarse a su cartera de hardware." }, "clickToRevealSeed": { "message": "Haga clic aquí para revelar las palabras secretas" @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Continuar a Wyre" }, + "contract": { + "message": "Contrato" + }, "contractAddressError": { "message": "Está enviando tokens a la dirección de contrato del token. Esto puede provocar la pérdida de los tokens." }, @@ -572,7 +579,7 @@ "message": "No volver a mostrar" }, "downloadGoogleChrome": { - "message": "Descargar Google Chrome" + "message": "Descargar Google Chrome" }, "downloadSecretBackup": { "message": "Descargue esta frase secreta de respaldo y guárdela en un medio de almacenamiento o disco duro externo cifrado." @@ -626,6 +633,10 @@ "endOfFlowMessage6": { "message": "Si necesita volver a crear una copia de seguridad de la frase secreta de recuperación, puede encontrarla en Configuración -> Seguridad." }, + "endOfFlowMessage7": { + "message": "Si tiene preguntas o nota movimientos sospechosos, comuníquese con soporte técnico $1.", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, "endOfFlowMessage8": { "message": "MetaMask no puede recuperar la frase secreta de recuperación." }, @@ -770,7 +781,7 @@ "message": "El límite de gas es la cantidad máxima de unidades de gas que está dispuesto a gastar." }, "gasLimitTooLow": { - "message": "El límite de gas debe ser al menos 21 000" + "message": "El límite de gas debe ser al menos 21 000" }, "gasLimitTooLowWithDynamicFee": { "message": "El límite de gas debe ser al menos $1", @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "Importar una cuenta con la frase secreta de recuperación" }, + "importAccountText": { + "message": "o $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "¿Desea importar el token?" + }, + "importTokenWarning": { + "message": "Toda persona puede crear un token con cualquier nombre, incluso versiones falsas de tokens existentes. ¡Agréguelo y realice transacciones bajo su propio riesgo!" + }, "importWallet": { "message": "Importar cartera" }, @@ -956,7 +977,7 @@ "message": "Número no válido. Quite todos los ceros iniciales." }, "invalidRPC": { - "message": "Dirección URL de RPC no válida" + "message": "Dirección URL de RPC no válida" }, "invalidSeedPhrase": { "message": "Frase secreta de recuperación no válida" @@ -1023,7 +1044,7 @@ "message": "Cargando tokens…" }, "localhost": { - "message": "Host local 8545" + "message": "Host local 8545" }, "lock": { "message": "Bloquear" @@ -1110,7 +1131,7 @@ "message": "Escriba su contraseña para confirmar que es usted." }, "mustSelectOne": { - "message": "Debe seleccionar al menos 1 token." + "message": "Debe seleccionar al menos 1 token." }, "myAccounts": { "message": "Mis cuentas" @@ -1160,10 +1181,10 @@ "message": "Agregar y editar redes RPC personalizadas" }, "networkURL": { - "message": "Dirección URL de la red" + "message": "Dirección URL de la red" }, "networkURLDefinition": { - "message": "La dirección URL que se utilizó para acceder a esta red." + "message": "La dirección URL que se utilizó para acceder a esta red." }, "networks": { "message": "Redes" @@ -1191,7 +1212,7 @@ "message": "Red nueva" }, "newPassword": { - "message": "Contraseña nueva (mín. de 8 caracteres)" + "message": "Contraseña nueva (mín. de 8 caracteres)" }, "newToMetaMask": { "message": "¿Es nuevo en MetaMask?" @@ -1287,6 +1308,22 @@ "message": "Su \"frase de recuperación\" ahora se llama \"frase secreta de recuperación.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "A partir de la versión 91 de Chrome, la API que habilitaba nuestro soporte para Ledger (U2F) ya no es compatible con carteras de hardware. MetaMask ha implementado un nuevo soporte para Ledger Live mediante el cual usted puede seguir conectándose a su dispositivo Ledger a través de la aplicación de escritorio Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Cuando interactúe con su cuenta de Ledger a través de MetaMask, se abrirá una nueva pestaña y se le pedirá que abra la aplicación Ledger Live. Una vez que se abra la aplicación, se le pedirá que otorgue permiso para establecer una conexión WebSocket con su cuenta de MetaMask. ¡Eso es todo!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "A fin de habilitar el soporte para Live Ledger, haga clic en Configuración > Avanzada > Utilizar Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Actualización del soporte para Ledger destinada a usuarios de Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "de" }, @@ -1372,7 +1409,7 @@ "message": "Moneda principal" }, "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." + "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." }, "privacyMsg": { "message": "Política de privacidad" @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Búsqueda, dirección pública (0x) o ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Iniciar aquí" + }, + "recoveryPhraseReminderConfirm": { + "message": "Entendido" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Guarde siempre su frase secreta de recuperación en un lugar seguro y secreto." + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "¿Necesita volver a crear una copia de seguridad de su frase secreta de recuperación?" + }, + "recoveryPhraseReminderItemOne": { + "message": "No comparta nunca su frase secreta de recuperación con nadie." + }, + "recoveryPhraseReminderItemTwo": { + "message": "El equipo de MetaMask nunca le pedirá su frase secreta de recuperación." + }, + "recoveryPhraseReminderSubText": { + "message": "Mediante su frase secreta de recuperación, se controlan todas sus cuentas." + }, + "recoveryPhraseReminderTitle": { + "message": "Proteja sus fondos." + }, "reject": { "message": "Rechazar" }, @@ -1421,7 +1482,7 @@ "message": "Rechazar todo" }, "rejectTxsDescription": { - "message": "Está a punto de rechazar $1 transacciones en lote." + "message": "Está a punto de rechazar $1 transacciones en lote." }, "rejectTxsN": { "message": "Rechazar $1 transacciones" @@ -1439,7 +1500,7 @@ "message": "Quitar cuenta" }, "removeAccountDescription": { - "message": "Esta cuenta se quitará de la cartera. Antes de continuar, asegúrese de tener la frase secreta de recuperación original o la clave privada de esta cuenta importada. Puede importar o crear cuentas nuevamente desde el menú desplegable de la cuenta." + "message": "Esta cuenta se quitará de la cartera. Antes de continuar, asegúrese de tener la frase secreta de recuperación original o la clave privada de esta cuenta importada. Puede importar o crear cuentas nuevamente en la lista desplegable de la cuenta. " }, "requestsAwaitingAcknowledgement": { "message": "solicitudes en espera de confirmación" @@ -1536,11 +1597,47 @@ "message": "Ingrese su frase secreta aquí para restaurar su bóveda." }, "securityAndPrivacy": { - "message": "Seguridad y privacidad" + "message": "Seguridad y privacidad" }, "securitySettingsDescription": { "message": "Configuración de privacidad y frase secreta de recuperación de la cartera" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Escríbala y guárdela en varios lugares secretos." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "Guárdela en un administrador de contraseñas" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "Guárdela en una caja fuerte." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "Guárdela en una bóveda bancaria." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "Su frase secreta de recuperación es la “llave maestra” de su cartera y sus fondos." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "Si alguien le pide su frase de recuperación, es posible que tenga intenciones de estafarlo." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Nunca comparta su frase secreta de recuperación, ni siquiera con MetaMask." + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "¿Qué es una frase de recuperación?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "¿Debería compartir mi frase de recuperación?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "¿Cómo guardo mi frase de recuperación?" + }, + "seedPhraseIntroTitle": { + "message": "Proteger su cartera" + }, + "seedPhraseIntroTitleCopy": { + "message": "Antes de comenzar, mire este breve video para aprender sobre su frase de recuperación y sobre cómo mantener segura su cartera." + }, "seedPhrasePlaceholder": { "message": "Separar cada palabra con un solo espacio" }, @@ -1548,7 +1645,7 @@ "message": "Pegar la frase secreta de recuperación desde el Portapapeles" }, "seedPhraseReq": { - "message": "Las frases secretas de recuperación contienen 12, 15, 18, 21 o 24 palabras" + "message": "Las frases secretas de recuperación contienen 12, 15, 18, 21 o 24 palabras" }, "selectAHigherGasFee": { "message": "Seleccione una cuota de gas más alta para acelerar el procesamiento de la transacción.*" @@ -1867,7 +1964,7 @@ "message": "Cuota de MetaMask" }, "swapMetaMaskFeeDescription": { - "message": "Buscamos el mejor precio en las fuentes de liquidez más importantes, todo el tiempo. Se incorpora de manera automática a esta cotización una cuota del $1 %.", + "message": "Buscamos el mejor precio en las fuentes de liquidez más importantes, todo el tiempo. Se incorpora de manera automática a esta cotización una cuota del $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNQuotes": { @@ -1890,9 +1987,18 @@ "description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts." }, "swapPriceDifferenceTitle": { - "message": "Diferencia de precio de ~$1 %", + "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." + }, + "swapPriceUnavailableDescription": { + "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el canje, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." + }, + "swapPriceUnavailableTitle": { + "message": "Antes de continuar, verifique su tasa" + }, "swapProcessing": { "message": "Procesamiento" }, @@ -1903,7 +2009,7 @@ "message": "Si el precio cambia entre el momento en que hace el pedido y cuando se confirma, se denomina \"desfase\". El canje se cancelará automáticamente si el desfase supera lo establecido en la configuración \"tolerancia de desfase\"." }, "swapQuoteIncludesRate": { - "message": "La cotización incluye una cuota de MetaMask de $1 %", + "message": "La cotización incluye una cuota de MetaMask de $1 %", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapQuoteNofN": { @@ -1994,6 +2100,9 @@ "message": "Canjear $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Este token se añadió de forma manual." + }, "swapTokenVerificationMessage": { "message": "Siempre confirme la dirección del token en $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2009,7 +2118,7 @@ "message": "Transacción completa" }, "swapTwoTransactions": { - "message": "2 transacciones" + "message": "2 transacciones" }, "swapUnknown": { "message": "Desconocido" @@ -2026,13 +2135,13 @@ "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" }, "swapZeroSlippage": { - "message": "0 % de desfase" + "message": "0 % de desfase" }, "swapsAdvancedOptions": { "message": "Opciones avanzadas" }, "swapsExcessiveSlippageWarning": { - "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." + "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." }, "swapsMaxSlippage": { "message": "Tolerancia de desfase" @@ -2072,7 +2181,7 @@ "message": "Símbolo" }, "symbolBetweenZeroTwelve": { - "message": "El símbolo debe tener 11 caracteres o menos." + "message": "El símbolo debe tener 11 caracteres o menos." }, "syncWithMobile": { "message": "Sincronizar con dispositivo móvil" @@ -2261,7 +2370,7 @@ "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, "urlExistsErrorMsg": { - "message": "La dirección URL ya está en la lista de redes existentes" + "message": "En este momento, la red $1 está utilizando esta dirección URL." }, "usePhishingDetection": { "message": "Usar detección de phishing" @@ -2283,6 +2392,10 @@ "message": "Comprobar este token en $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Ver cuenta" }, @@ -2317,7 +2430,7 @@ "message": "Frase secreta de recuperación de la cartera" }, "web3ShimUsageNotification": { - "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", + "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", "description": "$1 is a clickable link." }, "welcome": { diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index af16138e3..23d44c8af 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -6,7 +6,7 @@ "message": "Versión, centro de soporte técnico e información de contacto" }, "acceleratingATransaction": { - "message": "* Usar un precio de gas más alto para acelerar una transacción aumenta las posibilidades de un procesamiento más rápido en la red, pero esto no siempre se garantiza." + "message": "* Usar un precio de gas más alto para acelerar una transacción aumenta las posibilidades de un procesamiento más rápido en la red, pero esto no siempre se garantiza." }, "acceptTermsOfUse": { "message": "Leí y estoy de acuerdo con $1", @@ -52,6 +52,10 @@ "addContact": { "message": "Agregar contacto" }, + "addCustomTokenByContractAddress": { + "message": "¿No encuentra un token? Puede agregar cualquier token si copia su dirección. Puede encontrar la dirección de contrato del token en $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Esto permitirá que la red se utilice en MetaMask." }, @@ -85,7 +89,7 @@ "message": "Agregar a la libreta de direcciones" }, "addToAddressBookModalPlaceholder": { - "message": "p. ej., John D." + "message": "p. ej., John D." }, "addToken": { "message": "Agregar token" @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "El explorador no es compatible…" }, - "builContactList": { + "buildContactList": { "message": "Cree su lista de contactos" }, - "builtInCalifornia": { - "message": "MetaMask se diseñó y compiló en California." - }, "buy": { "message": "Comprar" }, @@ -268,7 +269,7 @@ "message": "Bytes" }, "canToggleInSettings": { - "message": "Puede volver a activar esta notificación desde Configuración > Alertas." + "message": "Puede volver a activar esta notificación desde Configuración -> Alertas." }, "cancel": { "message": "Cancelar" @@ -285,8 +286,11 @@ "chainIdDefinition": { "message": "El identificador de cadena que se utiliza para firmar transacciones en esta red." }, + "chainIdExistsErrorMsg": { + "message": "En este momento, la red $1 está utilizando este identificador de cadena." + }, "chromeRequiredForHardwareWallets": { - "message": "Debe usar MetaMask en Google Chrome para poder conectarse a su cartera de hardware." + "message": "Debe usar MetaMask en Google Chrome para poder conectarse a su cartera de hardware." }, "clickToRevealSeed": { "message": "Haga clic aquí para revelar las palabras secretas" @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Continuar a Wyre" }, + "contract": { + "message": "Contrato" + }, "contractAddressError": { "message": "Está enviando tokens a la dirección de contrato del token. Esto puede provocar la pérdida de los tokens." }, @@ -572,7 +579,7 @@ "message": "No volver a mostrar" }, "downloadGoogleChrome": { - "message": "Descargar Google Chrome" + "message": "Descargar Google Chrome" }, "downloadSecretBackup": { "message": "Descargue esta frase secreta de respaldo y guárdela en un medio de almacenamiento o disco duro externo cifrado." @@ -774,7 +781,7 @@ "message": "El límite de gas es la cantidad máxima de unidades de gas que está dispuesto a gastar." }, "gasLimitTooLow": { - "message": "El límite de gas debe ser al menos 21 000" + "message": "El límite de gas debe ser al menos 21 000" }, "gasLimitTooLowWithDynamicFee": { "message": "El límite de gas debe ser al menos $1", @@ -893,6 +900,12 @@ "message": "o $1", "description": "$1 represents the text from `importAccountLinkText` as a link" }, + "importTokenQuestion": { + "message": "¿Desea importar el token?" + }, + "importTokenWarning": { + "message": "Toda persona puede crear un token con cualquier nombre, incluso versiones falsas de tokens existentes. ¡Agréguelo y realice transacciones bajo su propio riesgo!" + }, "importWallet": { "message": "Importar cartera" }, @@ -964,7 +977,7 @@ "message": "Número no válido. Quite todos los ceros iniciales." }, "invalidRPC": { - "message": "Dirección URL de RPC no válida" + "message": "Dirección URL de RPC no válida" }, "invalidSeedPhrase": { "message": "Frase secreta de recuperación no válida" @@ -1031,7 +1044,7 @@ "message": "Cargando tokens…" }, "localhost": { - "message": "Host local 8545" + "message": "Host local 8545" }, "lock": { "message": "Bloquear" @@ -1118,7 +1131,7 @@ "message": "Escriba su contraseña para confirmar que es usted." }, "mustSelectOne": { - "message": "Debe seleccionar al menos 1 token." + "message": "Debe seleccionar al menos 1 token." }, "myAccounts": { "message": "Mis cuentas" @@ -1168,10 +1181,10 @@ "message": "Agregar y editar redes RPC personalizadas" }, "networkURL": { - "message": "Dirección URL de la red" + "message": "Dirección URL de la red" }, "networkURLDefinition": { - "message": "La dirección URL que se utilizó para acceder a esta red." + "message": "La dirección URL que se utilizó para acceder a esta red." }, "networks": { "message": "Redes" @@ -1199,7 +1212,7 @@ "message": "Red nueva" }, "newPassword": { - "message": "Contraseña nueva (mín. de 8 caracteres)" + "message": "Contraseña nueva (mín. de 8 caracteres)" }, "newToMetaMask": { "message": "¿Es nuevo en MetaMask?" @@ -1295,6 +1308,22 @@ "message": "Su \"frase de recuperación\" ahora se llama \"frase secreta de recuperación.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "A partir de la versión 91 de Chrome, la API que habilitaba nuestro soporte para Ledger (U2F) ya no es compatible con carteras de hardware. MetaMask ha implementado un nuevo soporte para Ledger Live mediante el cual usted puede seguir conectándose a su dispositivo Ledger a través de la aplicación de escritorio Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Cuando interactúe con su cuenta de Ledger a través de MetaMask, se abrirá una nueva pestaña y se le pedirá que abra la aplicación Ledger Live. Una vez que se abra la aplicación, se le pedirá que otorgue permiso para establecer una conexión WebSocket con su cuenta de MetaMask. ¡Eso es todo!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "A fin de habilitar el soporte para Live Ledger, haga clic en Configuración > Avanzada > Utilizar Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Actualización del soporte para Ledger destinada a usuarios de Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "de" }, @@ -1380,7 +1409,7 @@ "message": "Moneda principal" }, "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." + "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." }, "privacyMsg": { "message": "Política de privacidad" @@ -1422,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Búsqueda, dirección pública (0x) o ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Iniciar aquí" + }, + "recoveryPhraseReminderConfirm": { + "message": "Entendido" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Guarde siempre su frase secreta de recuperación en un lugar seguro y secreto." + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "¿Necesita volver a crear una copia de seguridad de su frase secreta de recuperación?" + }, + "recoveryPhraseReminderItemOne": { + "message": "No comparta nunca su frase secreta de recuperación con nadie." + }, + "recoveryPhraseReminderItemTwo": { + "message": "El equipo de MetaMask nunca le pedirá su frase secreta de recuperación." + }, + "recoveryPhraseReminderSubText": { + "message": "Mediante su frase secreta de recuperación, se controlan todas sus cuentas." + }, + "recoveryPhraseReminderTitle": { + "message": "Proteja sus fondos." + }, "reject": { "message": "Rechazar" }, @@ -1429,7 +1482,7 @@ "message": "Rechazar todo" }, "rejectTxsDescription": { - "message": "Está a punto de rechazar $1 transacciones en lote." + "message": "Está a punto de rechazar $1 transacciones en lote." }, "rejectTxsN": { "message": "Rechazar $1 transacciones" @@ -1544,7 +1597,7 @@ "message": "Ingrese su frase secreta aquí para restaurar su bóveda." }, "securityAndPrivacy": { - "message": "Seguridad y privacidad" + "message": "Seguridad y privacidad" }, "securitySettingsDescription": { "message": "Configuración de privacidad y frase secreta de recuperación de la cartera" @@ -1592,7 +1645,7 @@ "message": "Pegar la frase secreta de recuperación desde el Portapapeles" }, "seedPhraseReq": { - "message": "Las frases secretas de recuperación contienen 12, 15, 18, 21 o 24 palabras" + "message": "Las frases secretas de recuperación contienen 12, 15, 18, 21 o 24 palabras" }, "selectAHigherGasFee": { "message": "Seleccione una cuota de gas más alta para acelerar el procesamiento de la transacción.*" @@ -1911,7 +1964,7 @@ "message": "Cuota de MetaMask" }, "swapMetaMaskFeeDescription": { - "message": "Buscamos el mejor precio en las fuentes de liquidez más importantes, todo el tiempo. Se incorpora de manera automática a esta cotización una cuota del $1 %.", + "message": "Buscamos el mejor precio en las fuentes de liquidez más importantes, todo el tiempo. Se incorpora de manera automática a esta cotización una cuota del $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNQuotes": { @@ -1934,9 +1987,18 @@ "description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts." }, "swapPriceDifferenceTitle": { - "message": "Diferencia de precio de ~$1 %", + "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." + }, + "swapPriceUnavailableDescription": { + "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el canje, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." + }, + "swapPriceUnavailableTitle": { + "message": "Antes de continuar, verifique su tasa" + }, "swapProcessing": { "message": "Procesamiento" }, @@ -1947,7 +2009,7 @@ "message": "Si el precio cambia entre el momento en que hace el pedido y cuando se confirma, se denomina \"desfase\". El canje se cancelará automáticamente si el desfase supera lo establecido en la configuración \"tolerancia de desfase\"." }, "swapQuoteIncludesRate": { - "message": "La cotización incluye una cuota de MetaMask de $1 %", + "message": "La cotización incluye una cuota de MetaMask de $1 %", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapQuoteNofN": { @@ -2038,6 +2100,9 @@ "message": "Canjear $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Este token se añadió de forma manual." + }, "swapTokenVerificationMessage": { "message": "Siempre confirme la dirección del token en $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2053,7 +2118,7 @@ "message": "Transacción completa" }, "swapTwoTransactions": { - "message": "2 transacciones" + "message": "2 transacciones" }, "swapUnknown": { "message": "Desconocido" @@ -2070,13 +2135,13 @@ "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" }, "swapZeroSlippage": { - "message": "0 % de desfase" + "message": "0 % de desfase" }, "swapsAdvancedOptions": { "message": "Opciones avanzadas" }, "swapsExcessiveSlippageWarning": { - "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." + "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." }, "swapsMaxSlippage": { "message": "Tolerancia de desfase" @@ -2116,7 +2181,7 @@ "message": "Símbolo" }, "symbolBetweenZeroTwelve": { - "message": "El símbolo debe tener 11 caracteres o menos." + "message": "El símbolo debe tener 11 caracteres o menos." }, "syncWithMobile": { "message": "Sincronizar con dispositivo móvil" @@ -2305,7 +2370,7 @@ "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, "urlExistsErrorMsg": { - "message": "La dirección URL ya está en la lista de redes existentes" + "message": "En este momento, la red $1 está utilizando esta dirección URL." }, "usePhishingDetection": { "message": "Usar detección de phishing" @@ -2327,6 +2392,10 @@ "message": "Comprobar este token en $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Ver cuenta" }, @@ -2361,7 +2430,7 @@ "message": "Frase secreta de recuperación de la cartera" }, "web3ShimUsageNotification": { - "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", + "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", "description": "$1 is a clickable link." }, "welcome": { diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 36bac4a2f..23de7d7e1 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Teie lehitsejat ei toetata..." }, - "builtInCalifornia": { - "message": "MetaMask on projekteeritud ja loodud Californias." - }, "buyWithWyre": { "message": "Ostke ETH-d Wyre'iga" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 4d871ca55..d5230d513 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "مرورگر شما پشتیبانی نمیشود" }, - "builtInCalifornia": { - "message": "MetaMask در کالیفورنیا طراحی و ساخته شده است." - }, "buyWithWyre": { "message": "ETH را توسط Wyre خریداری نمایید" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index a2bc0c34d..f2a96b511 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Selaintasi ei tueta..." }, - "builtInCalifornia": { - "message": "MetaMask on suunniteltu ja koottu Kaliforniassa." - }, "buyWithWyre": { "message": "Osta ETH:ta Wyrella" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 43f7b79d7..d7ef62fa2 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -128,9 +128,6 @@ "browserNotSupported": { "message": "Hindi sinusuportahan ang iyong Browser..." }, - "builtInCalifornia": { - "message": "Ang MetaMask ay dinisenyo at binuo sa California." - }, "buyWithWyre": { "message": "Bumili ng ETH gamit ang Wyre" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 2e5bb49f9..31358aa71 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -137,9 +137,6 @@ "browserNotSupported": { "message": "Votre navigateur internet n'est pas supporté..." }, - "builtInCalifornia": { - "message": "MetaMask est designé et developpé en Californie." - }, "buyWithWyre": { "message": "Acheter ETH avec Wyre" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index f8ca504c0..795a25c53 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "הדפדפן שלך אינו נתמך..." }, - "builtInCalifornia": { - "message": "MetaMask תוכנן ונבנה בקליפורניה." - }, "buyWithWyre": { "message": "רכישת את'ר עם Wyre" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 6bc518358..4bb244a11 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "संपर्क जोड़ें" }, + "addCustomTokenByContractAddress": { + "message": "टोकन नहीं मिल रहा है? आप अपने पते को चिपकाकर मैन्युअल रूप से किसी भी टोकन को जोड़ सकते हैं। टोकन अनुबंध पते $1 पर मिल सकते हैं।", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "इससे इस नेटवर्क को MetaMask के अंदर उपयोग करने की अनुमति मिलेगी।" }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "आपका ब्राउज़र समर्थित नहीं है..." }, - "builContactList": { + "buildContactList": { "message": "अपनी संपर्क सूची बनाएं" }, - "builtInCalifornia": { - "message": "MetaMask को कैलिफोर्निया में डिज़ाइन और निर्मित किया गया है।" - }, "buy": { "message": "खरीदें" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "इस नेटवर्क के लिए लेन-देन पर हस्ताक्षर करने के लिए उपयोग की जाने वाली चेन ID।" }, + "chainIdExistsErrorMsg": { + "message": "यह चेन ID वर्तमान में $1 नेटवर्क द्वारा उपयोग किया जाता है।" + }, "chromeRequiredForHardwareWallets": { "message": "अपने हार्डवेयर वॉलेट से कनेक्ट करने के लिए आपको Google Chrome पर MetaMask का उपयोग करने की आवश्यकता है।" }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Wyre पर जारी रखें" }, + "contract": { + "message": "अनुबंध" + }, "contractAddressError": { "message": "आप टोकन के अनुबंध पते पर टोकन भेज रहे हैं। इसके परिणामस्वरूप इन टोकनों का नुकसान हो सकता है।" }, @@ -624,7 +631,11 @@ "message": "फ़िशिंग से सावधान रहें! MetaMask कभी भी अनायास ही आपके गुप्त रिकवरी फ्रेज़ के बारे में नहीं पूछेगा।" }, "endOfFlowMessage6": { - "message": "यदि आपको अपने गुप्त रिकवरी फ्रेज़ को फिर से बैकअप लेने की आवश्यकता है, तो आप इसे सेटिंग्स -> सुरक्षा में पा सकते हैं।" + "message": "यदि आपको अपने गुप्त रिकवरी फ्रेज़ को फिर से बैकअप लेने की आवश्यकता है, तो आप इसे सेटिंग -> सुरक्षा में पा सकते हैं।" + }, + "endOfFlowMessage7": { + "message": "यदि आपको कभी कुछ पूछना हो या कुछ गड़बड़ लगे, तो हमारी सहायता $1 से संपर्क करें।", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." }, "endOfFlowMessage8": { "message": "MetaMask आपके गुप्त रिकवरी फ्रेज़ को पुनर्प्राप्त नहीं कर सकता है।" @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "गुप्त रिकवरी फ्रेज़ के साथ एक खाता आयात करें" }, + "importAccountText": { + "message": "या $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "टोकन का आयात करें?" + }, + "importTokenWarning": { + "message": "कोई भी किसी भी नाम के साथ एक टोकन बना सकता है, जिसमें मौजूदा टोकन के नकली संस्करण शामिल हैं। अपने जोखिम पर जोड़ें और व्यापार करें!" + }, "importWallet": { "message": "वॉलेट आयात करें" }, @@ -1287,6 +1308,22 @@ "message": "आपके \"सीड फ्रेज़\" को अब आपका \"गुप्त रिकवरी फ्रेज़\" कहा जाता है।", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Chrome के संस्करण 91 से, वह API जो हमारे Ledger सपोर्ट (U2F) को सक्षम करती है वह अब हार्डवेयर वॉलेट का समर्थन नहीं करती। MetaMask ने एक नया Ledger Live सपोर्ट लागू किया है, जिसकी मदद से आप Ledger Live डेस्कटॉप ऐप के माध्यम से अपने Ledger डिवाइस से कनेक्ट करना जारी रख सकते हैं।", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "MetaMask में अपने Ledger खाते पर काम करते समय, एक नया टैब खुल जाएगा और आपको Ledger Live ऐप खोलने के लिए कहा जाएगा। ऐप खुलने के बाद, आपको अपने MetaMask खाते के लिए एक WebSocket कनेक्शन को अनुमति देने के लिए कहा जाएगा। बस इतना ही!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "आप सेटिंग > उन्नत > Ledger Live का उपयोग करें पर क्लिक करके Ledger Live सहायता को सक्षम कर सकते हैं।", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Chrome उपयोगकर्ताओं के लिए Ledger सहायता अद्यतन", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "/" }, @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "खोज, सार्वजनिक पता (0x) या ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "यहाँ से प्रारंभ करें" + }, + "recoveryPhraseReminderConfirm": { + "message": "समझ गया" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "अपने गुप्त रिकवरी फ्रेज़ को हमेशा सुरक्षित और गुप्त स्थान पर रखें।" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "अपने गुप्त रिकवरी फ्रेज़ को फिर से बैकअप करने की आवश्यकता है?" + }, + "recoveryPhraseReminderItemOne": { + "message": "कभी भी अपना गुप्त रिकवरी फ्रेज़ किसी के साथ साझा न करें" + }, + "recoveryPhraseReminderItemTwo": { + "message": "MetaMask टीम कभी भी आपके गुप्त रिकवरी फ्रेज़ के बारे में नहीं पूछेगा" + }, + "recoveryPhraseReminderSubText": { + "message": "आपका गुप्त रिकवरी फ्रेज़ आपके सभी खातों को नियंत्रित करता है।" + }, + "recoveryPhraseReminderTitle": { + "message": "अपने धन को सुरक्षित रखें" + }, "reject": { "message": "अस्वीकार करें" }, @@ -1541,6 +1602,42 @@ "securitySettingsDescription": { "message": "गोपनीयता सेटिंग्स और वॉलेट का गुप्त रिकवरी फ्रेज़" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "लिख लें और कई गुप्त स्थानों में स्टोर करें।" + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "पासवर्ड मैनेजर में सहेजें" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "सेफ़ डिपोज़िट बॉक्स में स्टोर करें।" + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "बैंक की तिजोरी में रखें।" + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "आपका रिकवरी फ्रेज़ आपके वॉलेट और धन के लिए “मास्टर कुंजी” है।" + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "यदि कोई व्यक्ति आपका रिकवरी फ्रेज़ मांगता है, तो सबसे अधिक संभावना है कि वे आपको धोखा देने का प्रयास कर रहे हैं।" + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "कभी भी अपना रिकवरी फ्रेज़ साझा न करें, MetaMask के साथ भी नहीं!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "रिकवरी फ्रेज़ क्या है?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "क्या मुझे अपना रिकवरी फ्रेज़ साझा करना चाहिए?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "मैं अपना रिकवरी फ्रेज़ कैसे सहेजूं?" + }, + "seedPhraseIntroTitle": { + "message": "अपने वॉलेट को सुरक्षित करें" + }, + "seedPhraseIntroTitleCopy": { + "message": "शुरुआत करने से पहले, अपने रिकवरी फ्रेज़ और अपने वॉलेट को सुरक्षित रखने के तरीके के बारे में जानने के लिए यह छोटा-सा वीडियो देखें।" + }, "seedPhrasePlaceholder": { "message": "प्रत्येक शब्द को एक रिक्ति से अलग करें" }, @@ -2003,6 +2100,9 @@ "message": "$1 से $2 में स्वैप करें", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "इस टोकन को मैन्युअल रूप से जोड़ा गया है।" + }, "swapTokenVerificationMessage": { "message": "हमेशा $1 पर टोकन पते की पुष्टि करें।", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2270,7 +2370,7 @@ "message": "URL को उपयुक्त HTTP/HTTPS उपसर्ग की आवश्यकता होती है।" }, "urlExistsErrorMsg": { - "message": "URL नेटवर्क की मौजूदा सूची में पहले से मौजूद है" + "message": "यह URL वर्तमान में $1 नेटवर्क द्वारा उपयोग किया जाता है।" }, "usePhishingDetection": { "message": "फ़िशिंग डिटेक्शन का उपयोग करें" @@ -2292,6 +2392,10 @@ "message": "इस टोकन को $1 पर सत्यापित करें", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "इस टोकन को $1 पर सत्यापित करें और सुनिश्चित करें कि यह वही टोकन है जिससे आप व्यापार करना चाहते हैं।", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "खाता देखें" }, @@ -2325,6 +2429,10 @@ "walletSeedRestore": { "message": "वॉलेट का गुप्त रिकवरी फ्रेज़" }, + "web3ShimUsageNotification": { + "message": "हमने देखा है कि वर्तमान वेबसाइट ने हटाए गए window.web3 API का उपयोग करने की कोशिश की। यदि साइट में गड़बड़ी लगती है, तो कृपया अधिक जानकारी के लिए $1 पर क्लिक करें।", + "description": "$1 is a clickable link." + }, "welcome": { "message": "MetaMask में आपका स्वागत है" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 2dfdfd179..d8aeceb8e 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -43,9 +43,6 @@ "blockiesIdenticon": { "message": "ब्लॉकीज पहचान का उपयोग करें" }, - "builtInCalifornia": { - "message": "मेटामास्क कैलिफ़ोर्निया में डिज़ाइन और बनाया गया है।" - }, "cancel": { "message": "रद्द करें" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 760043918..c56b5d88b 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Vaš se preglednik ne podržava..." }, - "builtInCalifornia": { - "message": "MetaMask je osmišljen i izrađen u Kaliforniji." - }, "buyWithWyre": { "message": "Kupi ETH Wyerom" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 61d5c1a96..78cc96a2a 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -73,9 +73,6 @@ "browserNotSupported": { "message": "Navigatè ou a pa sipòte..." }, - "builtInCalifornia": { - "message": "MetaMask fèt e bati nan California." - }, "cancel": { "message": "Anile" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index b124316cd..2293d2956 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Az ön böngészője nem támogatott..." }, - "builtInCalifornia": { - "message": "A MetaMaskot Kaliforniában tervezték és hozták létre." - }, "buyWithWyre": { "message": "Vásároljon ETH-t a Wyre-rel" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 07e913856..d5e42a1b0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "Tambah kontak" }, + "addCustomTokenByContractAddress": { + "message": "Tidak dapat menemukan token? Anda dapat menambahkan token secara manual dengan menempelkan alamatnya. Alamat kontrak token dapat ditemukan di $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Ini akan memungkinkan jaringan ini digunakan dengan MetaMask." }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "Browser Anda tidak didukung..." }, - "builContactList": { + "buildContactList": { "message": "Buat daftar kontak Anda" }, - "builtInCalifornia": { - "message": "MetaMask didesain dan didirikan di California." - }, "buy": { "message": "Beli" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "ID rantai digunakan untuk menandatangani transaksi untuk jaringan ini." }, + "chainIdExistsErrorMsg": { + "message": "ID Rantai ini saat ini digunakan oleh jaringan $1." + }, "chromeRequiredForHardwareWallets": { "message": "Anda perlu menggunakan MetaMask di Google Chrome untuk terhubung ke Dompet Perangkat Keras Anda." }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Lanjutkan ke Wyre" }, + "contract": { + "message": "Kontrak" + }, "contractAddressError": { "message": "Anda mengirim token ke alamat kontrak token. Ini dapat mengakibatkan token ini hilang." }, @@ -626,6 +633,10 @@ "endOfFlowMessage6": { "message": "Jika Anda perlu mencadangkan Frasa Pemulihan Rahasia lagi, Anda dapat menemukannya di Pengaturan -> Keamanan." }, + "endOfFlowMessage7": { + "message": "Jika Anda memiliki pertanyaan atau melihat sesuatu yang mencurigakan, hubungi dukungan $1 kami.", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, "endOfFlowMessage8": { "message": "MetaMask tidak dapat memulihkan Frasa Pemulihan Rahasia Anda." }, @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "Impor akun dengan Frasa Pemulihan Rahasia" }, + "importAccountText": { + "message": "atau $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "Impor token?" + }, + "importTokenWarning": { + "message": "Siapa pun dapat membuat token dengan nama apa pun, termasuk versi palsu dari token yang ada. Tambahkan dan perdagangkan dengan risiko Anda sendiri!" + }, "importWallet": { "message": "Impor dompet" }, @@ -1287,6 +1308,22 @@ "message": "\"Frasa Pemulihan\" Anda kini disebut \"Frasa Pemulihan Rahasia.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Pada Chrome versi 91, API yang memungkinkan dukungan Ledger (U2F) kami tidak lagi mendukung dompet perangkat keras. MetaMask telah menerapkan dukungan Ledger Live baru yang memungkinkan Anda terus terhubung ke perangkat Ledger Anda melalui aplikasi desktop Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Saat berinteraksi dengan akun Ledger Anda di MetaMask, tab baru akan terbuka dan Anda akan diminta untuk membuka aplikasi Ledger Live. Setelah aplikasi tersebut terbuka, Anda akan diminta untuk mengizinkan koneksi WebSocket ke akun MetaMask Anda. Tidak diperlukan tindakan lebih lanjut.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Anda dapat mengaktifkan dukungan Ledger Live dengan mengklik Pengaturan > Lanjutan > Gunakan Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Pembaruan Dukungan Ledger untuk Pengguna Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "dari" }, @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Cari, alamat publik (0x), atau ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Mulai di sini" + }, + "recoveryPhraseReminderConfirm": { + "message": "Mengerti" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Jaga selalu Frasa Pemulihan Rahasia Anda di tempat yang aman dan rahasia" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Perlu mencadangkan Frasa Pemulihan Rahasia Anda lagi?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Jangan membagikan Frasa Pemulihan Rahasia Anda kepada siapa pun" + }, + "recoveryPhraseReminderItemTwo": { + "message": "Tim MetaMask tidak akan pernah meminta Frasa Pemulihan Rahasia Anda" + }, + "recoveryPhraseReminderSubText": { + "message": "Frasa Pemulihan Rahasia Anda mengendalikan semua akun Anda." + }, + "recoveryPhraseReminderTitle": { + "message": "Lindungi dana Anda" + }, "reject": { "message": "Tolak" }, @@ -1439,7 +1500,7 @@ "message": "Hapus akun" }, "removeAccountDescription": { - "message": "Akun ini akan dihapus dari dompet Anda. Pastikan Anda memiliki Frasa Pemulihan Rahasia asli atau kunci privat untuk akun impor ini sebelum melanjutkan. Anda dapat mengimpor atau membuat akun lagi dari drop down akun. " + "message": "Akun ini akan dihapus dari dompet Anda. Pastikan Anda memiliki Frasa Pemulihan Rahasia asli atau kunci privat untuk akun impor ini sebelum melanjutkan. Anda dapat mengimpor atau membuat akun lagi dari akun drop down. " }, "requestsAwaitingAcknowledgement": { "message": "permintaan menunggu untuk diakui" @@ -1541,6 +1602,42 @@ "securitySettingsDescription": { "message": "Pengaturan privasi dan Frasa Pemulihan Rahasia dompet" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Tuliskan dan simpan di beberapa tempat rahasia." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "Simpan dalam pengelola kata sandi" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "Simpan di kotak deposit yang aman." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "Simpan di vault bank." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "Frasa pemulihan Anda adalah “kunci induk” ke dompet dan dana Anda." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "Jika seseorang menanyakan frasa pemulihan Anda, kemungkinan mereka akan mencoba menipu Anda." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Jangan pernah membagikan frasa pemulihan Anda bahkan kepada MetaMask!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "Apa itu frasa pemulihan?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "Haruskah saya membagikan frasa pemulihan saya?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "Bagaimana cara menyimpan frasa pemulihan saya?" + }, + "seedPhraseIntroTitle": { + "message": "Amankan dompet Anda" + }, + "seedPhraseIntroTitleCopy": { + "message": "Sebelum memulai, lihat video singkat ini untuk mempelajari tentang frasa pemulihan Anda dan cara menjaga keamanan dompet Anda." + }, "seedPhrasePlaceholder": { "message": "Pisahkan setiap kata dengan satu spasi" }, @@ -2003,6 +2100,9 @@ "message": "Tukar $1 untuk $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Token ini telah ditambahkan secara manual." + }, "swapTokenVerificationMessage": { "message": "Selalu konfirmasikan alamat token di $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2270,7 +2370,7 @@ "message": "URL memerlukan awalan HTTP/HTTPS yang sesuai." }, "urlExistsErrorMsg": { - "message": "URL sudah ada dalam daftar jaringan yang ada" + "message": "URL ini saat ini digunakan oleh jaringan $1." }, "usePhishingDetection": { "message": "Menggunakan Deteksi Phishing" @@ -2292,6 +2392,10 @@ "message": "Verifikasikan token ini di $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Verifikasi token ini di $1 dan pastikan ini adalah token yang ingin Anda perdagangkan.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Lihat Akun" }, @@ -2325,6 +2429,10 @@ "walletSeedRestore": { "message": "Frasa Pemulihan Rahasia Dompet" }, + "web3ShimUsageNotification": { + "message": "Kami melihat situs web saat ini mencoba menggunakan API window.web3 yang dihapus. Jika situs tersebut tampak bermasalah, silakan klik $1 untuk informasi selengkapnya.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Selamat datang di MetaMask" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 82c92fff4..f09f50b96 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -217,9 +217,6 @@ "browserNotSupported": { "message": "Il tuo Browser non è supportato..." }, - "builtInCalifornia": { - "message": "MetaMask è progettato e realizzato in California." - }, "buy": { "message": "Compra" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index c8b8d0a7c..bd73411ca 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "連絡先の追加" }, + "addCustomTokenByContractAddress": { + "message": "トークンを発見できませんか?アドレスをペーストすることで手動でトークンを追加することができます。トークン コントラクト アドレスは $1 にあります。", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "これにより、このネットワークは MetaMask 内で使用できるようになります。" }, @@ -109,7 +113,7 @@ "message": "アグリゲーター ネットワーク料金" }, "alertDisableTooltip": { - "message": "\"設定 > 警告\" の設定で変更できます" + "message": "これは、[\"設定 > 警告\"] で変更できます" }, "alertSettingsUnconnectedAccount": { "message": "選択した未接続のアカウントを使用して Web サイトをブラウズしています" @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "ご使用のブラウザーはサポートされていません..." }, - "builContactList": { + "buildContactList": { "message": "連絡先リストを作成する" }, - "builtInCalifornia": { - "message": "MetaMask はカリフォルニアで設計および作成されました。" - }, "buy": { "message": "購入" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "このネットワークのトランザクションの署名に使用されるチェーン ID。" }, + "chainIdExistsErrorMsg": { + "message": "このチェーン ID は現在 $1 ネットワークで使用しています。" + }, "chromeRequiredForHardwareWallets": { "message": "ハードウェア ウォレットに接続するには、MetaMask on Google Chrome を使用する必要があります。" }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Wyre に進む" }, + "contract": { + "message": "コントラクト" + }, "contractAddressError": { "message": "トークンのコントラクト アドレスにトークンを送信しています。これにより、これらのトークンが失われる可能性があります。" }, @@ -624,7 +631,11 @@ "message": "フィッシングにご注意ください!MetaMask の動作として、シークレット リカバリー フレーズを要求することは絶対にありません。" }, "endOfFlowMessage6": { - "message": "シークレット リカバリー フレーズを再度バックアップする場合は、[設定] -> [セキュリティとプライバシー] にアクセスしてください。" + "message": "シークレット リカバリー フレーズを再度バックアップする場合は、[設定] -> [セキュリティ] でそれを見つけることができます。" + }, + "endOfFlowMessage7": { + "message": "ご質問、または不審な点がある場合は、当社のサポート $1 までお問い合わせください。", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." }, "endOfFlowMessage8": { "message": "MetaMask はシークレット リカバリー フレーズを復元できません。" @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "シークレット リカバリー フレーズを使用してアカウントをインポートする:" }, + "importAccountText": { + "message": "または $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "トークンをインポートしますか?" + }, + "importTokenWarning": { + "message": "誰でも既存のトークンの偽バージョンを含めて、任意の名前でトークンを作成することができます。自己責任で追加およびトレードしてください。" + }, "importWallet": { "message": "ウォレットのインポート" }, @@ -1287,6 +1308,22 @@ "message": "これで、\"シード フレーズ\" は \"シークレット リカバリー フレーズ\" と呼ばれます。", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Chrome バージョン 91 以降は、レジャーのサポート (U2F) を可能にした API がハードウェア ウォレットをサポートしなくなります。MetaMask では、ユーザーがレジャー ライブのデスクトップ アプリを介して、レジャー デバイスに継続的に接続することができる新しいレジャー ライブのサポートを導入しました。", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "MetaMask のレジャーのアカウントを使用する際は、新しいタブが開き、レジャー ライブのアプリを開くよう指示されます。アプリが開いたら、WebSocket 接続を MetaMask のアカウントに許可するよう指示されます。以上です。", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "[設定] > [詳細] > [レジャー ライブを使用] の順にクリックすることで、レジャー ライブのサポートを有効にすることができます。", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Chrome ユーザー向けのレジャーのサポートの更新", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "中の" }, @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "検索、パブリック アドレス (0x)、または ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "ここから開始" + }, + "recoveryPhraseReminderConfirm": { + "message": "OK" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "シークレット リカバリー フレーズは常に安全かつ秘密の場所に保管してください" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "シークレット リカバリー フレーズのバックアップが必要ですか?" + }, + "recoveryPhraseReminderItemOne": { + "message": "シークレット リカバリー フレーズは誰とも決して共有しないでください" + }, + "recoveryPhraseReminderItemTwo": { + "message": "MetaMask チームが、ユーザーのシークレット リカバリー フレーズを確認することは絶対にありません" + }, + "recoveryPhraseReminderSubText": { + "message": "シークレット リカバリー フレーズは、ご利用のすべてのアカウントを制御します。" + }, + "recoveryPhraseReminderTitle": { + "message": "資産を保護してください" + }, "reject": { "message": "拒否" }, @@ -1541,6 +1602,42 @@ "securitySettingsDescription": { "message": "プライバシーの設定とシークレット リカバリー フレーズ" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "書き留めて、複数の秘密の場所に保存します。" + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "パスワード マネージャーに保存する" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "セーフティ ボックスに保管する。" + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "銀行の金庫に保管する。" + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "あなたのリカバリー フレーズは、ウォレットと資金への「マスターキー」です。" + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "誰かがあなたのリカバリー フレーズを尋ねてきたら、おそらくあなたを騙そうとしているのです。" + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "MetaMask を共有しても、リカバリ フレーズは決して共有しないでください。" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "リカバリー フレーズとは何ですか?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "リカバリーフレーズは共有すべきですか?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "リカバリー フレーズはどのように保存すべきですか?" + }, + "seedPhraseIntroTitle": { + "message": "ウォレットの保護" + }, + "seedPhraseIntroTitleCopy": { + "message": "始める前に、この短いビデオを見て、リカバリー フレーズとウォレットを安全に保つ方法について確認してください。" + }, "seedPhrasePlaceholder": { "message": "単語ごとにスペースを 1 つ置いて分離します" }, @@ -1893,6 +1990,15 @@ "message": "約 $1% の価格差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "プライスインパクトとは、現在の市場価格と取引の約定時に受け取った金額の差のことです。プライスインパクトとは、流動性プールの大きさに対するあなたのトレードの大きさを表わす関数です。" + }, + "swapPriceUnavailableDescription": { + "message": "市場価格のデータが不足しているため、プライスインパクトを測定できませんでした。スワップする前に、これから受領するトークンの額に問題がないか確認してください。" + }, + "swapPriceUnavailableTitle": { + "message": "続行する前にレートを確認してください" + }, "swapProcessing": { "message": "処理中" }, @@ -1994,6 +2100,9 @@ "message": "$1 を $2 にスワップ", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "このトークンは手動で追加されました。" + }, "swapTokenVerificationMessage": { "message": "常に $1 のトークン アドレスを確認してください。", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2261,7 +2370,7 @@ "message": "URL には適切な HTTP/HTTPS プレフィックスが必要です。" }, "urlExistsErrorMsg": { - "message": "URL はネットワークの既存のリストに既に存在します" + "message": "この URL は現在 $1 ネットワークで使用しています。" }, "usePhishingDetection": { "message": "フィッシング検出を使用" @@ -2283,6 +2392,10 @@ "message": "このトークンを $1 で検証", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "このトークンを $1 で検証して、それがトレードしたいトークンであることを確認してください。", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "アカウントを表示" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index dff966fe2..9f3b17c66 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "ನಿಮ್ಮ ಬ್ರೌಸರ್ ಬೆಂಬಲಿಸುತ್ತಿಲ್ಲ..." }, - "builtInCalifornia": { - "message": "MetaMask ಅನ್ನು ವಿನ್ಯಾಸಗೊಳಿಸಲಾಗಿದೆ ಮತ್ತು ಕ್ಯಾಲಿಫೋರ್ನಿಯಾದಲ್ಲಿ ನಿರ್ಮಿಸಲಾಗಿದೆ." - }, "buyWithWyre": { "message": "Wyre ನೊಂದಿಗೆ ETH ಖರೀದಿಸಿ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index a14259717..0cd69a0f5 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -6,7 +6,7 @@ "message": "버전, 지원 센터, 연락처" }, "acceleratingATransaction": { - "message": "* 높은 Gas 가격을 이용한 거래 가속화는 네트워크를 통한 처리 속도 개선 확률을 높이지만, 항상 그렇지는 않습니다." + "message": "* 높은 Gas 가격을 이용해 거래를 가속화하면 네트워크를 통한 처리 속도가 개선되지만 항상 그렇지는 않습니다." }, "acceptTermsOfUse": { "message": "$1을(를) 읽고 이에 동의합니다.", @@ -52,22 +52,26 @@ "addContact": { "message": "연락처 추가" }, + "addCustomTokenByContractAddress": { + "message": "이 토큰을 찾을 수 없으신가요? 토큰 주소를 붙여넣으면 토큰을 직접 추가할 수 있습니다. 토큰의 계약 주소는 $1에서 찾을 수 있습니다.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { - "message": "이는 이 네트워크가 MetaMask 내에서 사용될 수 있도록 합니다." + "message": "이렇게 하면 이 네트워크가 MetaMask 내에서 사용됩니다." }, "addEthereumChainConfirmationRisks": { "message": "MetaMask는 맞춤형 네트워크를 검증하지 않습니다." }, "addEthereumChainConfirmationRisksLearnMore": { - "message": "$1에 대해 자세히 알아보십시오.", + "message": "$1에 대해 자세히 알아보세요.", "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" }, "addEthereumChainConfirmationRisksLearnMoreLink": { - "message": "사기와 보안 위험에 대해 자세히 알아보기", + "message": "사기와 네트워크 보안 위험", "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" }, "addEthereumChainConfirmationTitle": { - "message": "이 사이트에서 네트워크를 추가하도록 허용하시겠습니까?" + "message": "이 사이트에서 네트워크를 추가하도록 허용하시겠어요?" }, "addFriendsAndAddresses": { "message": "신뢰하는 친구와 주소 추가하기" @@ -106,7 +110,7 @@ "message": "동의함" }, "aggregatorFeeCost": { - "message": "애그리게이터 네트워크 요금" + "message": "애그리게이터 네트워크 수수료" }, "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다." @@ -118,10 +122,10 @@ "message": "이 경고는 연결된 web3 사이트를 탐색하고 있지만 현재 선택된 계정이 연결되지 않은 경우 팝업에 표시됩니다." }, "alertSettingsWeb3ShimUsage": { - "message": "웹사이트가 제거된 window.web3 API를 이용하려 할 때" + "message": "웹사이트가 제거된 window.web3 API를 이용하는 경우" }, "alertSettingsWeb3ShimUsageDescription": { - "message": "이 경고는 제거된 window.web3 API를 이용하는 사이트를 탐색할 때 팝업에 표시되며 손상이 발생할 수 있습니다." + "message": "이 경고는 제거된 window.web3 API를 이용하려고 시도하여 결과적으로 작동하지 않을 수 있는 사이트를 탐색할 때 팝업으로 표시됩니다." }, "alerts": { "message": "경고" @@ -133,7 +137,7 @@ "message": "이 외부 확장을 통해 다음을 하도록 허용:" }, "allowOriginSpendToken": { - "message": "$1에서 $2을(를) 사용하도록 허용하시겠습니까?", + "message": "$1에서 $2을(를) 지출하도록 허용하시겠어요?", "description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend" }, "allowThisSiteTo": { @@ -158,7 +162,7 @@ "description": "The name of the application" }, "approvalAndAggregatorTxFeeCost": { - "message": "승인 및 애그리게이터 네트워크 요금" + "message": "승인 및 애그리게이터 네트워크 수수료" }, "approvalTxGasCost": { "message": "승인 Tx Gas 비용" @@ -186,13 +190,13 @@ "message": "자산" }, "attemptToCancel": { - "message": "취소하시겠습니까?" + "message": "취소하시겠어요?" }, "attemptToCancelDescription": { "message": "이 시도를 제출한다고 해서 원래 거래가 반드시 취소되지는 않습니다. 취소 시도가 성공하면 위의 거래 수수료가 부과됩니다." }, "attemptingConnect": { - "message": "블록체인 연결 시도 중입니다." + "message": "블록체인에 연결 중입니다." }, "attributions": { "message": "속성" @@ -216,10 +220,10 @@ "message": "전체 목록으로 돌아가기" }, "backupApprovalInfo": { - "message": "이 비밀 코드는 장치를 분실하여 지갑을 복구하거나, 암호를 잊거나, MetaMask를 다시 설치해야 하거나, 다른 장치에서 지갑에 액세스해야 할 때 필요합니다." + "message": "이 비밀 코드는 장치를 분실하여 지갑을 복구해야 하거나, 암호를 잊었거나, MetaMask를 다시 설치해야 하거나, 다른 장치에서 지갑에 액세스해야 할 때 필요합니다." }, "backupApprovalNotice": { - "message": "비밀 복구 코드를 백업하여 지갑과 자금을 안전하게 보호하십시오." + "message": "시드 코드를 백업하여 지갑과 자금을 안전하게 보호하세요." }, "backupNow": { "message": "지금 백업" @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "지원되지 않는 브라우저입니다..." }, - "builContactList": { + "buildContactList": { "message": "연락처 목록 작성하기" }, - "builtInCalifornia": { - "message": "MetaMask는 캘리포니아에서 설계 및 제작됩니다." - }, "buy": { "message": "구매" }, @@ -285,11 +286,14 @@ "chainIdDefinition": { "message": "이 네트워크의 거래에 서명하는 데 사용되는 체인 ID입니다." }, + "chainIdExistsErrorMsg": { + "message": "이 체인 ID는 현재 $1 네트워크에서 사용됩니다." + }, "chromeRequiredForHardwareWallets": { "message": "하드웨어 지갑에 연결하려면 Google Chrome에서 MetaMask를 사용해야 합니다." }, "clickToRevealSeed": { - "message": "암호를 표시하려면 여기를 클릭하세요" + "message": "비밀 단어를 표시하려면 여기를 클릭하세요." }, "close": { "message": "닫기" @@ -307,13 +311,13 @@ "message": "확인됨" }, "confusableUnicode": { - "message": "'$1'은 '$2'와 유사합니다." + "message": "'$1'은(는) '$2'와(과) 유사합니다." }, "confusableZeroWidthUnicode": { "message": "폭이 0인 문자를 발견했습니다." }, "confusingEnsDomain": { - "message": "ENS 이름에서 혼동하기 쉬운 문자를 발견했습니다. 잠재적 사기를 막기 위해 ENS 이름을 확인하십시오." + "message": "ENS 이름에서 혼동하기 쉬운 문자를 발견했습니다. 잠재적 사기를 막기 위해 ENS 이름을 확인하세요." }, "congratulations": { "message": "축하합니다." @@ -347,7 +351,7 @@ "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" }, "connectToMultipleNumberOfAccounts": { - "message": "$1 계정", + "message": "$1개 계정", "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" }, "connectWithMetaMask": { @@ -367,7 +371,7 @@ "message": "연결된 사이트" }, "connectedSitesDescription": { - "message": "$1이(가) 이 사이트에 연결되어 있습니다. 귀하의 계정 주소를 볼 수 있습니다.", + "message": "$1이(가) 이 사이트에 연결되어 귀하의 계정 주소를 볼 수 있습니다.", "description": "$1 is the account name" }, "connectedSitesEmptyDescription": { @@ -396,7 +400,7 @@ "message": "Ropsten 테스트 네트워크에 연결 중" }, "contactUs": { - "message": "당사에 문의하세요" + "message": "문의하기" }, "contacts": { "message": "연락처" @@ -410,8 +414,11 @@ "continueToWyre": { "message": "Wyre로 넘어가기" }, + "contract": { + "message": "계약" + }, "contractAddressError": { - "message": "토큰의 계약 주소로 토큰을 보냅니다. 토큰이 손실될 수 있습니다." + "message": "토큰의 계약 주소로 토큰을 보냅니다. 이로 인해 토큰이 손실될 수 있습니다." }, "contractDeployment": { "message": "계약 배포" @@ -471,7 +478,7 @@ "message": "Gas 맞춤화" }, "customGasSubTitle": { - "message": "요금을 올리면 처리 시간이 줄어들 수 있지만 반드시 그렇지는 않습니다." + "message": "수수료를 올리면 처리 시간이 단축되기도 하지만 항상 그렇지는 않습니다." }, "customRPC": { "message": "맞춤형 RPC" @@ -483,7 +490,7 @@ "message": "맞춤형 토큰" }, "dataBackupFoundInfo": { - "message": "계정 데이터 일부가 MetaMask 이전 설치 도중에 백업되었습니다. 여기에는 설정, 연락처, 토큰이 포함될 수 있습니다. 지금 이 데이터를 복구하시겠습니까?" + "message": "일부 계정 데이터가 이전의 MetaMask 설치 도중에 백업되었습니다. 여기에는 설정, 연락처, 토큰이 포함될 수 있습니다. 지금 이 데이터를 복구하시겠어요?" }, "decimal": { "message": "토큰 십진수" @@ -512,7 +519,7 @@ "message": "요청 암호 해독" }, "defaultNetwork": { - "message": "Ether 거래의 기존 네트워크는 메인 넷입니다." + "message": "Ether 거래의 기본 네트워크는 메인 넷입니다." }, "delete": { "message": "삭제" @@ -521,10 +528,10 @@ "message": "계정 삭제" }, "deleteNetwork": { - "message": "네트워크를 삭제합니까?" + "message": "네트워크를 삭제하시겠어요?" }, "deleteNetworkDescription": { - "message": "이 네트워크를 삭제하시겠습니까?" + "message": "이 네트워크를 삭제하시겠어요?" }, "depositEther": { "message": "Ether 예치" @@ -545,7 +552,7 @@ "message": "모든 계정 연결 해제" }, "disconnectAllAccountsConfirmationDescription": { - "message": "이 네트워크의 연결을 해제하시겠습니까? 사이트 기능을 이용하지 못하게 될 수도 있습니다." + "message": "연결을 해제하시겠어요? 사이트 기능을 이용하지 못하게 될 수도 있습니다." }, "disconnectPrompt": { "message": "$1 연결 해제" @@ -557,10 +564,10 @@ "message": "해지" }, "dismissReminderDescriptionField": { - "message": "이것을 켜서 복구 구문 백업 알림 메시지를 해지하십시오. 지갑을 복원할 수 있도록 비밀 복구 구문을 저장할 것을 강력하게 권장합니다." + "message": "이 기능을 켜면 복구 구문 백업 알림 메시지를 해지할 수 있습니다. 단, 자금 손실을 방지하려면 비밀 복구 구문을 백업하는 것이 좋습니다." }, "dismissReminderField": { - "message": "복구 구문 백업 알림을 해지하십시오." + "message": "복구 구문 백업 알림 해지" }, "domain": { "message": "도메인" @@ -569,7 +576,7 @@ "message": "완료" }, "dontShowThisAgain": { - "message": "이 메시지를 다시 표시하지 않음" + "message": "다시 표시 안 함" }, "downloadGoogleChrome": { "message": "Google Chrome 다운로드" @@ -593,20 +600,20 @@ "message": "임시값 편집" }, "editNonceMessage": { - "message": "이것은 고급 기능으로, 주의해서 사용해야 합니다." + "message": "이는 고급 기능으로, 주의해서 사용해야 합니다." }, "editPermission": { "message": "권한 편집" }, "encryptionPublicKeyNotice": { - "message": "$1에서 귀하의 공개 암호화 키를 요구합니다. 동의하면 이 사이트에서 암호화된 메시지를 작성하여 귀하에게 전송할 수 있습니다.", + "message": "$1에서 귀하의 공개 암호화 키를 요구합니다. 동의를 받으면 이 사이트에서 암호화된 메시지를 작성하여 귀하에게 전송할 수 있습니다.", "description": "$1 is the web3 site name" }, "encryptionPublicKeyRequest": { "message": "암호화 공개 키 요구" }, "endOfFlowMessage1": { - "message": "테스트를 통과하셨습니다. 비밀 복구 구문을 안전하게 보관할 책임은 귀하에게 있습니다!" + "message": "테스트를 통과하셨습니다. 비밀 복구 구문을 안전하게 보관할 책임은 본인에게 있습니다." }, "endOfFlowMessage10": { "message": "모두 완료" @@ -621,10 +628,14 @@ "message": "구문을 누구와도 공유하지 마세요." }, "endOfFlowMessage5": { - "message": "피싱을 조심하십시오! MetaMask에서는 절대로 시드 구문을 갑자기 물어보지 않습니다." + "message": "피싱에 유의하세요. MetaMask에서는 절대로 비밀 복구 구문을 갑자기 물어보지 않습니다." }, "endOfFlowMessage6": { - "message": "비밀 복구 구문을 다시 백업해야 한다면 설정 -> 보안에서 시드 구문을 찾을 수 있습니다." + "message": "비밀 복구 구문을 다시 백업해야 한다면 설정 -> 보안에서 해당 구문을 찾을 수 있습니다." + }, + "endOfFlowMessage7": { + "message": "질문이 있거나 의심스러운 행위를 목격했다면 지원을 요청하세요($1).", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." }, "endOfFlowMessage8": { "message": "MetaMask에서는 계정 시드 구문을 복구할 수 없습니다." @@ -690,7 +701,7 @@ "message": "예상 처리 시간" }, "ethGasPriceFetchWarning": { - "message": "현재 주요 가스 견적 서비스를 사용할 수 없으므로 백업 가스 가격을 제공합니다." + "message": "현재 주요 Gas 견적 서비스를 사용할 수 없으므로 백업 Gas 가격을 제공합니다." }, "eth_accounts": { "message": "허용되는 계정의 주소 보기(필수)", @@ -715,7 +726,7 @@ "message": "외부 확장" }, "extraApprovalGas": { - "message": "+$1 승인 Gas", + "message": "+$1의 승인 Gas", "description": "Expresses an additional gas amount the user will have to pay, on top of some other displayed amount. $1 is a decimal amount of gas" }, "failed": { @@ -734,14 +745,14 @@ "message": "가장 빠름" }, "feeAssociatedRequest": { - "message": "요금이 이 권한과 연결되어 있습니다." + "message": "수수료가 이 요청과 연결되어 있습니다." }, "fiat": { "message": "명목", "description": "Exchange type" }, "fileImportFail": { - "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요!", + "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.", "description": "Helps user import their account from a JSON file" }, "forbiddenIpfsGateway": { @@ -780,7 +791,7 @@ "message": "Gas 가격(GWEI)" }, "gasPriceExcessive": { - "message": "Gas 수수료가 불필요하게 높게 설정되었습니다. 수량을 낮추는 것을 고려해 보십시오." + "message": "Gas 수수료가 불필요하게 높게 설정되었습니다. 수량을 낮추는 것을 고려해 보세요." }, "gasPriceExcessiveInput": { "message": "Gas 가격이 초과하였습니다." @@ -798,11 +809,11 @@ "message": "사용한 Gas" }, "gdprMessage": { - "message": "이 데이터는 집계되므로 개인정보보호 규정(EU) 2016/679의 목적에 따라 익명으로 관리됩니다. 당사 개인정보 보호 관행과 관련된 자세한 내용은 $1을(를) 참조하세요.", + "message": "이 데이터는 집계되며 일반 데이터 보호 규칙(EU) 2016/679의 목적에 따라 익명으로 관리됩니다. 당사의 개인정보보호 관행에 관한 자세한 내용은 $1을(를) 참조하세요.", "description": "$1 refers to the gdprMessagePrivacyPolicy message, the translation of which is meant to be used exclusively in the context of gdprMessage" }, "gdprMessagePrivacyPolicy": { - "message": "개인정보 보호정책", + "message": "개인정보보호정책", "description": "this translation is intended to be exclusively used as the replacement for the $1 in the gdprMessage translation" }, "general": { @@ -834,17 +845,17 @@ "message": "하드웨어 지갑 연결됨" }, "hardwareWalletLegacyDescription": { - "message": "(래거시)", + "message": "(레거시)", "description": "Text representing the MEW path" }, "hardwareWalletSupportLinkConversion": { - "message": "여기를 클릭하세요." + "message": "여기를 클릭" }, "hardwareWallets": { "message": "하드웨어 지갑 연결" }, "hardwareWalletsMsg": { - "message": "MetaMask와 함께 사용할 하드웨어 지갑을 선택하십시오." + "message": "MetaMask와 함께 사용할 하드웨어 지갑을 선택하세요." }, "here": { "message": "여기", @@ -857,7 +868,7 @@ "message": "숨기기" }, "hideTokenPrompt": { - "message": "토큰을 숨기시겠습니까?" + "message": "토큰을 숨기시겠어요?" }, "hideTokenSymbol": { "message": "$1 숨기기", @@ -877,34 +888,40 @@ "message": "계정 가져오기" }, "importAccountLinkText": { - "message": "계정 시드 구문으로 가져오기" + "message": "비밀 복구 구문을 사용해 가져오기" }, "importAccountMsg": { - "message": " 가져온 계정은 생성한 MetaMask 계정 시드 구문 원본에 연결되지 않습니다. 가져온 계정에 대해 자세히 알아보십시오. " + "message": " 가져온 계정은 생성한 MetaMask 계정 비밀 복구 구문 원본에 연결되지 않습니다. 가져온 계정에 대해 자세히 알아보기 " }, "importAccountSeedPhrase": { - "message": "시드 구문으로 계정 가져오기" + "message": "비밀 복구 구문으로 계정 가져오기" }, "importAccountText": { "message": "또는 $1", "description": "$1 represents the text from `importAccountLinkText` as a link" }, + "importTokenQuestion": { + "message": "토큰을 가져오시겠어요?" + }, + "importTokenWarning": { + "message": "기존 토큰의 가짜 버전을 포함하여 누구나 어떤 이름으로든 토큰을 만들 수 있습니다. 추가 및 거래는 사용자의 책임입니다." + }, "importWallet": { "message": "지갑 가져오기" }, "importYourExisting": { - "message": "12단어 시드 구문을 사용하여 지갑 가져오기" + "message": "비밀 복구 구문을 사용하여 기존 지갑 가져오기" }, "imported": { "message": "가져옴", "description": "status showing that an account has been fully loaded into the keyring" }, "infuraBlockedNotification": { - "message": "MetaMask이 블록체인 호스트에 연결할 수 없습니다. 가능성 있는 원인 $1을 검토하십시오.", + "message": "MetaMask이 블록체인 호스트에 연결할 수 없습니다. 가능성 있는 원인 $1을(를) 검토하세요.", "description": "$1 is a clickable link with with text defined by the 'here' key" }, "initialTransactionConfirmed": { - "message": "최초 거래를 네트워크에서 확인했습니다. 확인을 클릭하여 뒤로 돌아가세요." + "message": "최초 거래를 네트워크에서 확인했습니다. 돌아가려면 확인을 클릭하세요." }, "insufficientBalance": { "message": "잔액이 부족합니다." @@ -931,7 +948,7 @@ "message": "잘못된 체인 ID. 체인 ID가 너무 큽니다." }, "invalidCustomNetworkAlertContent1": { - "message": "맞춤형 네트워크 '$1의 체인 ID를 다시 입력해야 합니다.", + "message": "맞춤형 네트워크 '$1'의 체인 ID를 다시 입력해야 합니다.", "description": "$1 is the name/identifier of the network." }, "invalidCustomNetworkAlertContent2": { @@ -963,7 +980,7 @@ "message": "잘못된 RPC URL" }, "invalidSeedPhrase": { - "message": "잘못된 계정 시드 구문" + "message": "잘못된 비밀 복구 구문" }, "ipfsGateway": { "message": "IPFS 게이트웨이" @@ -979,7 +996,7 @@ "message": "알려진 계약 주소입니다." }, "knownTokenWarning": { - "message": "이 작업은 피싱에 사용할 수 있는, 지갑에 이미 나열된 토큰을 편집합니다. 이 토큰이 나타내는 내용을 변경해야 할 때만 작업을 승인하세요." + "message": "이 작업은 지갑에 이미 나열되어 있고 피싱에 사용될 수 있는 토큰을 편집합니다. 해당 토큰이 나타내는 내용을 변경하려는 경우에만 작업을 승인하세요." }, "kovan": { "message": "Kovan 테스트 네트워크" @@ -997,28 +1014,28 @@ "message": "Ledger Live 사용하기" }, "ledgerLiveAdvancedSettingDescription": { - "message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능." + "message": "새로운 Ledger Live 브리지를 통해 Ledger를 더 쉽게 사용할 수 있습니다. Chrome에서만 사용 가능합니다." }, "ledgerLiveApp": { "message": "Ledger Live 앱" }, "ledgerLocked": { - "message": "Ledger 장치에 연결할 수 없습니다. 장치의 잠금이 해제되어 있고 이더리움 앱이 열려 있는지 확인하십시오." + "message": "Ledger 장치에 연결할 수 없습니다. 장치의 잠금이 해제되어 있고 이더리움 앱이 열려 있는지 확인하세요." }, "ledgerTimeout": { - "message": "Ledger Live의 응답 시간이 너무 길거나 연결 시간을 초과하였습니다. Ledger Live가 열려있고 장치의 잠금이 해제되어 있는지 확인하십시오." + "message": "Ledger Live의 응답 시간이 너무 길거나 연결 시간을 초과하였습니다. Ledger Live 앱이 열려 있고 장치의 잠금이 해제되어 있는지 확인하세요." }, "letsGoSetUp": { "message": "설정을 시작하죠!" }, "likeToAddTokens": { - "message": "이 토큰을 추가하시겠습니까?" + "message": "이 토큰을 추가하시겠어요?" }, "links": { "message": "링크" }, "loadMore": { - "message": "추가 항목 로드" + "message": "추가 로드" }, "loading": { "message": "로드 중..." @@ -1033,7 +1050,7 @@ "message": "잠금" }, "lockTimeTooGreat": { - "message": "자금 시간이 너무 김" + "message": "잠금 시간이 너무 깁니다." }, "mainnet": { "message": "이더리움 메인넷" @@ -1054,13 +1071,13 @@ "message": "메시지" }, "metaMaskConnectStatusParagraphOne": { - "message": "MetaMask의 계정 연결에 대한 제어 기능이 강화되었습니다." + "message": "이제 MetaMask의 계정 연결을 더 효과적으로 제어할 수 있습니다." }, "metaMaskConnectStatusParagraphThree": { "message": "클릭하여 연결된 계정을 관리하세요." }, "metaMaskConnectStatusParagraphTwo": { - "message": "방문 중인 웹사이트가 현재 선택된 계정에 연결되어 있다면 연결 상태 버튼이 표시됩니다." + "message": "방문 중인 웹사이트가 현재 선택한 계정에 연결되어 있다면 연결 상태 버튼이 표시됩니다." }, "metamaskDescription": { "message": "이더리움 및 분산형 웹에 연결합니다." @@ -1082,15 +1099,15 @@ "message": "MetaMask에서는.." }, "metametricsCommitmentsNeverCollectIP": { - "message": "$1은(는) 전체 IP 주소를 수집하지 않습니다.", + "message": "전체 IP 주소를 절대 수집하지 않습니다.", "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" }, "metametricsCommitmentsNeverCollectKeysEtc": { - "message": "$1은(는) 키, 주소, 거래, 잔액, 해시 또는 개인 정보를 수집합니다.", + "message": "키, 주소, 거래, 잔액, 해시 또는 개인 정보를 절대 수집하지 않습니다.", "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" }, "metametricsCommitmentsNeverSellDataForProfit": { - "message": "$1은(는) 수익을 위해 데이터를 판매합니다. 절대로요!", + "message": "수익을 위해 데이터를 절대 판매하지 않습니다. 결코 그렇습니다.", "description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'" }, "metametricsCommitmentsSendAnonymizedEvents": { @@ -1100,7 +1117,7 @@ "message": "MetaMask 개선에 참여" }, "metametricsOptInDescription": { - "message": "MetaMask는 사용자의 확장 사용 방식을 더 잘 이해하기 위해 사용 데이터를 수집하려 합니다. 이 데이터는 제품과 이더리움 생태계의 사용 편의성과 사용자 경험을 지속적으로 개선하는 데 사용됩니다." + "message": "MetaMask는 사용자가 확장 프로그램과 상호작용하는 방식을 자세히 이해하기 위해 사용 데이터를 수집하려 합니다. 이 데이터는 당사의 제품과 이더리움 에코시스템의 사용 편의성 및 사용자 경험을 지속적으로 개선하는 데 사용됩니다." }, "mismatchedChain": { "message": "이 체인 ID의 네트워크 세부 정보가 기록과 일치하지 않습니다. 진행하기 전에 $1을(를) 권장합니다.", @@ -1111,7 +1128,7 @@ "description": "Serves as link text for the 'mismatchedChain' key. This text will be embedded inside the translation for that key." }, "mobileSyncText": { - "message": "암호를 입력하여 본인임을 확인하세요!" + "message": "암호를 입력하여 본인임을 인증하세요." }, "mustSelectOne": { "message": "토큰을 1개 이상 선택해야 합니다." @@ -1126,7 +1143,7 @@ "message": "MetaMask를 이용하는 분산형 애플리케이션과 상호작용하려면 지갑에 Ether가 있어야 합니다." }, "needHelp": { - "message": "도움이 필요하십니까? $1에 문의하십시오.", + "message": "도움이 필요하신가요? $1에 문의하세요.", "description": "$1 represents `needHelpLinkText`, the text which goes in the help link" }, "needHelpLinkText": { @@ -1158,7 +1175,7 @@ "message": "테스트넷" }, "networkSettingsChainIdDescription": { - "message": "체인 ID는 거래 서명에 사용합니다. 네트워크에서 반환하는 체인 ID와 일치해야 합니다. 십진수나 '0x'로 시작하는 16진수를 입력할 수 있지만, 표시될 때는 십진수로 표시됩니다." + "message": "체인 ID는 거래 서명에 사용됩니다. 이는 네트워크에서 반환하는 체인 ID와 일치해야 합니다. 십진수나 '0x'로 시작하는 16진수를 입력할 수 있지만, 표시되는 형식은 십진수입니다." }, "networkSettingsDescription": { "message": "맞춤형 RPC 네트워크 추가 및 편집" @@ -1167,7 +1184,7 @@ "message": "네트워크 URL" }, "networkURLDefinition": { - "message": "이 네트워크에 접근하기 위한 URL입니다." + "message": "이 네트워크에 액세스하는 데 사용되는 URL입니다." }, "networks": { "message": "네트워크" @@ -1210,20 +1227,20 @@ "message": "다음" }, "nextNonceWarning": { - "message": "임시값이 추천 임시값($1)보다 큼", + "message": "임시값이 추천 임시값($1)보다 큽니다.", "description": "The next nonce according to MetaMask's internal logic" }, "noAccountsFound": { - "message": "검색 쿼리에 맞는 계정 없음" + "message": "검색어에 해당하는 계정이 없습니다." }, "noAddressForName": { "message": "이 이름에 설정된 주소가 없습니다." }, "noAlreadyHaveSeed": { - "message": "아니요. 이미 시드 구문이 있습니다." + "message": "아니요. 이미 비밀 복구 구문이 있습니다." }, "noConversionRateAvailable": { - "message": "사용 가능한 전환율 없음" + "message": "사용 가능한 전환율이 없음" }, "noThanks": { "message": "괜찮습니다" @@ -1238,19 +1255,19 @@ "message": "웹캠을 찾을 수 없음" }, "nonce": { - "message": "임시" + "message": "임시값" }, "nonceField": { "message": "거래 임시값 맞춤화" }, "nonceFieldDescription": { - "message": "이 기능을 켜면 확인 화면에서 임시값(거래 번호)을 변경할 수 있습니다. 이것은 고급 기능으로, 주의해서 사용해야 합니다." + "message": "이 기능을 켜면 확인 화면에서 임시값(거래 번호)을 변경할 수 있습니다. 이는 고급 기능으로, 주의해서 사용해야 합니다." }, "nonceFieldHeading": { - "message": "맞춤 임시값" + "message": "맞춤형 임시값" }, "notCurrentAccount": { - "message": "이(가) 올바른 계정인가요? 지갑에서 현재 선택된 계정과 다릅니다." + "message": "올바른 계정인가요? 현재 지갑에서 선택된 계정과 다릅니다." }, "notEnoughGas": { "message": "Gas 부족" @@ -1268,7 +1285,7 @@ "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website." }, "notifications3Description": { - "message": "MetaMask 보안에 대한 모범 사례의 최신 정보를 얻고 공식 MetaMask 지원에서 최신 보안 팁을 확인하십시오.", + "message": "MetaMask 보안에 대한 모범 사례의 최신 정보를 얻고 공식 MetaMask 지원에서 최신 보안 팁을 확인하세요.", "description": "Description of a notification in the 'See What's New' popup. Describes the information they can get on security from the linked support page." }, "notifications3Title": { @@ -1280,7 +1297,7 @@ "description": "The 'call to action' on the button, or link, of the 'Swap on Binance Smart Chain!' notification. Upon clicking, users will be taken to a page where then can swap tokens on Binance Smart Chain." }, "notifications4Description": { - "message": "지갑에서 토큰 스왑 최고가를 바로 이용하십시오. MetaMask는 이제 바이낸스 스마트 체인의 여러 분산형 교환 애그리게이터 및 투자전문기관과 연결됩니다.", + "message": "지갑에서 토큰 스왑 최고가를 바로 이용하세요. MetaMask는 이제 바이낸스 스마트 체인의 여러 분산형 교환 애그리게이터 및 투자전문기관과 연결됩니다.", "description": "Description of a notification in the 'See What's New' popup." }, "notifications4Title": { @@ -1291,6 +1308,22 @@ "message": "\"시드 구문\"을 이제 \"계정 시드 구문\"이라고 합니다.", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Chrome 버전 91부터 Ledger 지원(U2F)을 활성화한 API에서 하드웨어 지갑을 지원하지 않습니다. MetaMask는 Ledger Live 데스크톱 앱을 통해 Ledger 장치에 계속 연결할 수 있는 새로운 Ledger Live 지원을 구축했습니다.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "MetaMask에서 Ledger 계정과 상호작용하면 새 탭이 열리고 Ledger Live 앱을 열라는 메시지가 표시됩니다. 앱이 열리면 MetaMask 계정에 대한 WebSocket 연결을 허용하라는 메시지가 표시됩니다. 이제 다 됐습니다.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Ledger Live 지원은 설정 > 고급 > Ledger Live 사용을 클릭하여 활성화할 수 있습니다.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Chrome 사용자용 Ledger 지원 업데이트", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "/" }, @@ -1317,7 +1350,7 @@ "message": "메인넷에서만 사용 가능" }, "onlyConnectTrust": { - "message": "신뢰하는 사이트에만 연결하세요." + "message": "신뢰하는 사이트만 연결하세요." }, "optionalBlockExplorerUrl": { "message": "블록 탐색기 URL(선택 사항)" @@ -1354,10 +1387,10 @@ "message": "보류 중" }, "permissionCheckedIconDescription": { - "message": "이 권한을 수락하셨습니다." + "message": "이 권한을 승인했습니다." }, "permissionUncheckedIconDescription": { - "message": "이 권한을 수락하지 않으셨습니다." + "message": "이 권한을 승인하지 않았습니다." }, "permissions": { "message": "권한" @@ -1366,7 +1399,7 @@ "message": "개인 주소가 발견되었습니다. 토큰 계약 주소를 입력하세요." }, "plusXMore": { - "message": "+ 외 $1개", + "message": "+ 그 외 $1개", "description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items" }, "prev": { @@ -1379,7 +1412,7 @@ "message": "체인의 고유 통화(예: ETH)로 값을 우선 표시하려면 고유를 선택합니다. 선택한 명목 통화로 값을 우선 표시하려면 명목을 선택합니다." }, "privacyMsg": { - "message": "개인정보 보호정책" + "message": "개인정보보호정책" }, "privateKey": { "message": "비공개 키", @@ -1407,7 +1440,7 @@ "message": "대기열에 지정됨" }, "readdToken": { - "message": "나중에 계정 옵션 메뉴의 “토큰 추가”로 이동하면 이 토큰을 다시 추가할 수 있습니다." + "message": "나중에 계정 옵션 메뉴의 '토큰 추가'로 이동하면 이 토큰을 다시 추가할 수 있습니다." }, "receive": { "message": "받기" @@ -1418,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "검색, 공개 주소(0x) 또는 ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "여기에서 시작" + }, + "recoveryPhraseReminderConfirm": { + "message": "확인" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "계정 시드 구문은 언제나 보안이 유지되고 알려지지 않은 곳에 보관해야 합니다." + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "계정 시드 구문을 다시 백업해야 합니까?" + }, + "recoveryPhraseReminderItemOne": { + "message": "절대로 다른 사람과 계정 시드 구문을 공유하지 마십시오" + }, + "recoveryPhraseReminderItemTwo": { + "message": "MetaMask 팀에서는 절대로 계정 시드 구문을 물어보지 않습니다" + }, + "recoveryPhraseReminderSubText": { + "message": "계정 시드 구문으로 귀하의 모든 계정을 관리합니다." + }, + "recoveryPhraseReminderTitle": { + "message": "자금을 지키세요" + }, "reject": { "message": "거부" }, @@ -1443,7 +1500,7 @@ "message": "계정 제거" }, "removeAccountDescription": { - "message": "이 계정이 지갑에서 제거됩니다. 계속하기 전에 가져온 이 계정에 대한 원본 시드 구문이나 비공개 키가 있는지 확인하십시오. 계정 드롭다운에서 계정을 가져오거나 다시 만들 수 있습니다. " + "message": "이 계정이 지갑에서 제거됩니다. 계속하기 전에 가져온 이 계정에 대한 원본 비밀 복구 구문이나 비공개 키가 있는지 확인하세요. 계정 드롭다운에서 계정을 가져오거나 다시 만들 수 있습니다. " }, "requestsAwaitingAcknowledgement": { "message": "확인 대기 중인 요청" @@ -1458,32 +1515,32 @@ "message": "계정 재설정" }, "resetAccountDescription": { - "message": "계정을 재설정하면 거래 내역이 지워집니다. 계정의 잔액이 변경되지 않으면 시드 구문을 다시 입력하지 않아도 됩니다." + "message": "계정을 재설정하면 거래 내역이 지워집니다. 계정의 잔액은 변경되지 않으며 비밀 복구 구문을 다시 입력하지 않아도 됩니다." }, "restore": { "message": "복구" }, "restoreAccountWithSeed": { - "message": "시드 구문으로 계정 복구" + "message": "비밀 복구 구문으로 계정 복구" }, "restoreWalletPreferences": { - "message": "$1의 데이터 백업이 발견되었습니다. 지갑 환경설정을 복구하시겠습니까?", + "message": "$1의 데이터 백업이 발견되었습니다. 지갑 환경설정을 복원하시겠어요?", "description": "$1 is the date at which the data was backed up" }, "retryTransaction": { "message": "거래 재시도" }, "reusedTokenNameWarning": { - "message": "여기에 있는 토큰은 사용자가 확인한 다른 토큰의 기호를 재사용하기 때문에 혼란스럽거나 속기 쉽습니다." + "message": "여기에 있는 토큰은 사용자가 주시 중인 다른 토큰의 기호를 재사용하기 때문에 혼동되거나 속기 쉽습니다." }, "revealSeedWords": { - "message": "계정 시드 구문 공개" + "message": "비밀 복구 구문 공개" }, "revealSeedWordsDescription": { - "message": "브라우저를 변경하거나 컴퓨터를 옮긴 경우, 계정에 액세스하려면 이 시드 구문이 필요합니다. 기밀이 보장된 안전한 곳에 보관하십시오." + "message": "브라우저를 변경하거나 컴퓨터를 옮긴 경우, 계정에 액세스하려면 이 비밀 복구 구문이 필요합니다. 기밀이 보장된 안전한 곳에 보관하세요." }, "revealSeedWordsTitle": { - "message": "계정 시드 구문" + "message": "비밀 복구 구문" }, "revealSeedWordsWarning": { "message": "이 구문은 계정 전체를 도용하는 데 사용될 수 있습니다." @@ -1543,10 +1600,10 @@ "message": "보안 및 개인정보 보호" }, "securitySettingsDescription": { - "message": "개인정보 설정 및 지갑 시드 구문" + "message": "개인정보 설정 및 지갑 비밀 복구 구문" }, "seedPhraseIntroSidebarBulletFour": { - "message": "적어서 여러 비밀 장소에 보관." + "message": "적어서 여러 비밀 장소에 보관하세요." }, "seedPhraseIntroSidebarBulletOne": { "message": "암호 관리자에 저장" @@ -1558,40 +1615,40 @@ "message": "은행 금고에 보관." }, "seedPhraseIntroSidebarCopyOne": { - "message": "복구 구문은 지갑과 펀드의 “마스터 키” 입니다." + "message": "복구 구문은 지갑과 자금의 '마스터 키'입니다." }, "seedPhraseIntroSidebarCopyThree": { - "message": "복구 구문을 요청하는 사람은 사기를 치려고 하는 것입니다." + "message": "복구 구문을 요청하는 사람은 사기를 치려는 것입니다." }, "seedPhraseIntroSidebarCopyTwo": { - "message": "절대로, MetaMask와도 복구 구문을 공유하면 안 됩니다!" + "message": "절대로, MetaMask와도 시드 구문을 공유하면 안 됩니다!" }, "seedPhraseIntroSidebarTitleOne": { - "message": "'복구 구문'이란 무엇입니까?" + "message": "'복구 구문'이란 무엇인가요?" }, "seedPhraseIntroSidebarTitleThree": { - "message": "복구 구문을 공유해야 합니까?" + "message": "복구 구문을 공유해야 하나요?" }, "seedPhraseIntroSidebarTitleTwo": { - "message": "복구 구문을 어떻게 저장합니까?" + "message": "복구 구문은 어떻게 저장하나요?" }, "seedPhraseIntroTitle": { "message": "지갑 보호하기" }, "seedPhraseIntroTitleCopy": { - "message": "시작하기 전에 이 비디오를 통해 복구 구문과 지갑을 보호하는 방법에 대해 알아보십시오." + "message": "시작하기 전에 이 짧은 동영상을 보고 복구 구문과 지갑을 안전하게 보호하는 방법에 대해 알아보세요." }, "seedPhrasePlaceholder": { - "message": "공백 한 칸으로 각 단어를 구분하십시오." + "message": "공백 한 칸으로 각 단어를 구분하세요." }, "seedPhrasePlaceholderPaste": { - "message": "클립보드에서 시드 구문 붙여넣기" + "message": "클립보드에서 비밀 복구 구문 붙여넣기" }, "seedPhraseReq": { - "message": "시드 구문에 12, 15, 18, 21 또는 24단어 포함" + "message": "비밀 복구 구문은 12, 15, 18, 21 또는 24개의 단어를 포함합니다." }, "selectAHigherGasFee": { - "message": "높은 가스 요금을 선택하면 거래 처리 속도를 높일 수 있습니다.*" + "message": "높은 Gas 수수료를 선택하면 거래 처리 속도를 높일 수 있습니다.*" }, "selectAccounts": { "message": "계정 선택" @@ -1631,7 +1688,7 @@ "message": "보내기" }, "sendAmount": { - "message": "금액 보내기" + "message": "송금" }, "sendSpecifiedTokens": { "message": "$1 보내기", @@ -1644,7 +1701,7 @@ "message": "Ether 보냄" }, "separateEachWord": { - "message": "공백 한 칸으로 각 단어를 구분하십시오." + "message": "공백 한 칸으로 각 단어를 구분하세요." }, "settings": { "message": "설정" @@ -1653,13 +1710,13 @@ "message": "고급 Gas 제어 기능" }, "showAdvancedGasInlineDescription": { - "message": "이 항목을 선택하면 보내기 및 확인 화면에 Gas 가격이 표시되며 제어 기능을 바로 제한할 수 있습니다." + "message": "이 항목을 선택하면 보내기 및 확인 화면에서 바로 Gas 가격을 표시하고 제어 기능을 제한할 수 있습니다." }, "showFiatConversionInTestnets": { "message": "테스트넷에 전환 표시" }, "showFiatConversionInTestnetsDescription": { - "message": "이 항목을 선택하면 테스트넷에 명목 전환을 표시합니다." + "message": "이 항목을 선택하면 테스트넷에 명목 전환이 표시됩니다." }, "showHexData": { "message": "16진수 데이터 표시" @@ -1671,7 +1728,7 @@ "message": "수신 거래 표시" }, "showIncomingTransactionsDescription": { - "message": "이 항목을 선택하면 Etherscan을 사용하여 거래 목록에 수신 거래를 표시합니다." + "message": "이 항목을 선택하면 Etherscan을 사용해 거래 목록에 수신 거래를 표시할 수 있습니다." }, "showPermissions": { "message": "권한 표시" @@ -1680,7 +1737,7 @@ "message": "비공개 키 표시" }, "showSeedPhrase": { - "message": "계정 시드 구문 표시" + "message": "비밀 복구 구문 표시" }, "sigRequest": { "message": "서명 요청" @@ -1756,21 +1813,21 @@ "message": "Ledger 앱 다운로드" }, "step1LedgerWalletMsg": { - "message": "$1의 잠금을 해제하기 위해 다운로드, 설정 및 암호를 입력하세요.", + "message": "$1의 잠금을 해제하려면 다운로드, 설정 및 암호를 입력하세요.", "description": "$1 represents the `ledgerLiveApp` localization value" }, "step1TrezorWallet": { "message": "Trezor 지갑 연결" }, "step1TrezorWalletMsg": { - "message": "지갑을 컴퓨터에 바로 연결합니다. 하드웨어 지갑 장치를 사용하기 위한 더 많은 내용은 $1", + "message": "지갑을 컴퓨터에 바로 연결합니다. 하드웨어 지갑 장치를 사용하는 방법에 관한 자세한 내용은 $1", "description": "$1 represents the `hardwareWalletSupportLinkConversion` localization key" }, "step2LedgerWallet": { "message": "Ledger 지갑 연결" }, "step2LedgerWalletMsg": { - "message": "지갑을 컴퓨터에 바로 연결합니다. Ledger를 잠금 해제하고 Ethereum 앱을 엽니다. 하드웨어 지갑 장치를 사용하기 위한 더 많은 내용은 $1.", + "message": "지갑을 컴퓨터에 바로 연결합니다. Ledger를 잠금 해제하고 Ethereum 앱을 엽니다. 하드웨어 지갑 장치를 사용하는 방법에 관한 자세한 내용은 $1.", "description": "$1 represents the `hardwareWalletSupportLinkConversion` localization key" }, "storePhrase": { @@ -1792,7 +1849,7 @@ "message": "스왑" }, "swapAdvancedSlippageInfo": { - "message": "주문 시점과 확인 시점 사이에 가격이 변동되는 현상을 “슬리패지”라고 합니다. 슬리패지가 “최대 슬리패지” 설정을 초과하면 스왑이 자동으로 취소됩니다." + "message": "주문 시점과 확인 시점 사이에 가격이 변동되는 현상을 '슬리패지'라고 합니다. 슬리패지가 '최대 슬리패지' 설정을 초과하면 스왑이 자동으로 취소됩니다." }, "swapAggregator": { "message": "애그리게이터" @@ -1808,18 +1865,18 @@ "message": "수신하는 최소 금액입니다. 슬리패지에 따라 추가 금액을 받을 수도 있습니다." }, "swapApproval": { - "message": "$1 스왑 승인", + "message": "스왑을 위해 $1 승인", "description": "Used in the transaction display list to describe a transaction that is an approve call on a token that is to be swapped.. $1 is the symbol of a token that has been approved." }, "swapApproveNeedMoreTokens": { - "message": "이 스왑을 완료하려면 $1와(과) 추가 $2이(가) 필요합니다.", + "message": "이 스왑을 완료하려면 $1개의 추가 $2이(가) 필요합니다.", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, "swapBetterQuoteAvailable": { "message": "더 나은 견적이 있습니다." }, "swapBuildQuotePlaceHolderText": { - "message": "$1와(과) 일치하는 가용 토큰 없음", + "message": "$1와(과) 일치하는 토큰이 없습니다.", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, "swapCheckingQuote": { @@ -1845,21 +1902,21 @@ "message": "한도 편집" }, "swapEnableDescription": { - "message": "필수이며 MetaMask에게 $1을(를) 스왑할 권한을 제공합니다.", + "message": "필수이며 MetaMask에게 $1을(를) 스왑할 권한을 부여합니다.", "description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps." }, "swapEstimatedNetworkFee": { - "message": "예상 네트워크 요금" + "message": "예상 네트워크 수수료" }, "swapEstimatedNetworkFeeSummary": { - "message": "“$1”은(는) 당사가 예상하는 실제 요금입니다. 정확한 금액은 네트워크 상태에 따라 달라집니다.", + "message": "'$1'은(는) 당사가 예상하는 실제 수수료입니다. 정확한 금액은 네트워크 상태에 따라 달라집니다.", "description": "$1 will be the translation of swapEstimatedNetworkFee, with the font bolded" }, "swapEstimatedNetworkFees": { - "message": "예상 네트워크 요금" + "message": "예상 네트워크 수수료" }, "swapEstimatedNetworkFeesInfo": { - "message": "스왑을 완료하는 데 사용할 네트워크 요금 예상치입니다. 실제 금액은 네트워크 조건에 따라 달라질 수 있습니다." + "message": "스왑을 완료하는 데 사용될 예상 네트워크 수수료입니다. 실제 금액은 네트워크 상태에 따라 달라질 수 있습니다." }, "swapFailedErrorDescriptionWithSupportLink": { "message": "거래가 실패할 경우 언제든 문의하세요. 오류가 해결되지 않는다면 고객 지원 $1에 문의하세요.", @@ -1872,7 +1929,7 @@ "message": "음.... 문제가 발생했습니다. 다시 시도해 보고 오류가 해결되지 않는다면 고객 지원에 문의하세요." }, "swapFetchingQuotesErrorTitle": { - "message": "견적 가져오는 중 오류 발생" + "message": "견적을 가져오는 중 오류 발생" }, "swapFetchingTokens": { "message": "토큰 가져오는 중..." @@ -1881,11 +1938,11 @@ "message": "마무리 중..." }, "swapFromTo": { - "message": "$1를 $2로 스왑", + "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" }, "swapGasFeesSplit": { - "message": "이전 화면의 Gas 요금은 이 두 거래로 나뉩니다." + "message": "이전 화면의 Gas 수수료는 이 두 거래로 나뉩니다." }, "swapHighSlippageWarning": { "message": "슬리패지 금액이 아주 큽니다." @@ -1898,16 +1955,16 @@ "description": "$1 will be the translation of swapMaxNetworkFees, with the font bolded" }, "swapMaxNetworkFees": { - "message": "최대 네트워크 요금" + "message": "최대 네트워크 수수료" }, "swapMaxSlippage": { "message": "최대 슬리패지" }, "swapMetaMaskFee": { - "message": "MetaMask 요금" + "message": "MetaMask 수수료" }, "swapMetaMaskFeeDescription": { - "message": "당사는 매번 최상의 유동성 소스에서 최적 가격을 찾습니다. 이 견적에는 $1%의 수수료가 자동으로 반영됩니다.", + "message": "당사는 매번 최상의 유동성 소스에서 최적의 가격을 찾습니다. 이 견적에는 $1%의 수수료가 자동으로 반영됩니다.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNQuotes": { @@ -1915,7 +1972,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "네트워크 요금에는 스왑을 처리하고 $1 네트워크에 보관하는 비용이 적용됩니다. MetaMask는 이 요금을 이용해 이득을 얻지 않습니다." + "message": "네트워크 수수료에는 스왑을 처리하고 $1 네트워크에 보관하는 비용이 포함됩니다. MetaMask는 이 수수료로 수익을 얻지 않습니다." }, "swapNewQuoteIn": { "message": "$1의 새 견적", @@ -1926,7 +1983,7 @@ "description": "This message communicates the token that is being transferred. It is shown on the awaiting swap screen. The $1 will be a token symbol." }, "swapPriceDifference": { - "message": "$1 $2 (~$3)을 $4 $5 (~$6)로 스왑합니다.", + "message": "$1 $2(~$3)을(를) $4 $5(~$6)(으)로 스왑하려고 합니다.", "description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts." }, "swapPriceDifferenceTitle": { @@ -1937,10 +1994,10 @@ "message": "가격 영향은 현재 시장 가격과 거래 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 거래의 크기를 나타내는 함수입니다." }, "swapPriceUnavailableDescription": { - "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수에 만족하시는지 확인하시기 바랍니다." + "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 확인하시기 바랍니다." }, "swapPriceUnavailableTitle": { - "message": "진행하기 전에 요율을 확인하십시오." + "message": "진행하기 전에 요율 확인" }, "swapProcessing": { "message": "처리 중" @@ -1966,7 +2023,7 @@ "message": "견적은 현재 시장 상황을 반영하도록 자주 갱신됩니다." }, "swapQuotesExpiredErrorDescription": { - "message": "지금 견적을 요청해 최신 요율을 확인하세요." + "message": "새 견적을 요청해 최신 요율을 확인하세요." }, "swapQuotesExpiredErrorTitle": { "message": "견적 시간 초과" @@ -2014,7 +2071,7 @@ "message": "유동성 소스" }, "swapSourceInfo": { - "message": "당사에서는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 요율과 최저 네트워크 요금을 찾습니다." + "message": "당사에서는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 요율과 최저 네트워크 수수료를 찾습니다." }, "swapSwapFrom": { "message": "다음에서 스왑" @@ -2043,15 +2100,18 @@ "message": "$1에서 $2(으)로 스왑", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "이 토큰은 수동으로 추가되었습니다." + }, "swapTokenVerificationMessage": { - "message": "항상 $1에서 토큰 주소를 확인하십시오.", + "message": "항상 $1에서 토큰 주소를 확인하세요.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." }, "swapTokenVerificationOnlyOneSource": { "message": "1개의 소스에서만 확인됩니다." }, "swapTokenVerificationSources": { - "message": "$1 소스에서 확인되었습니다.", + "message": "$1개 소스에서 확인되었습니다.", "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." }, "swapTransactionComplete": { @@ -2081,13 +2141,13 @@ "message": "고급 옵션" }, "swapsExcessiveSlippageWarning": { - "message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 최대 슬리패지를 15% 값 이하로 줄이십시오." + "message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리패지 허용치를 15% 값 이하로 줄이세요." }, "swapsMaxSlippage": { - "message": "최대 슬리패지" + "message": "슬리패지 허용치" }, "swapsNotEnoughForTx": { - "message": "$1이(가) 부족하여 이 거래를 완료할 수 없음", + "message": "$1이(가) 부족하여 이 거래를 완료할 수 없습니다.", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" }, "swapsViewInActivity": { @@ -2097,13 +2157,13 @@ "message": "이는 MetaMask 내의 선택된 네트워크를 이전에 추가된 다음 네트워크로 전환합니다." }, "switchEthereumChainConfirmationTitle": { - "message": "이 사이트가 네트워크를 전환하도록 허용합니까?" + "message": "이 사이트가 네트워크를 전환하도록 허용하시겠어요?" }, "switchLedgerPaths": { "message": "Ledger 경로 전환" }, "switchLedgerPathsText": { - "message": "다른 계정을 보려면 Ledger 경로 선택" + "message": "다른 계정을 보려면 Ledger 경로를 선택하세요." }, "switchNetwork": { "message": "네트워크 전환" @@ -2203,16 +2263,16 @@ "message": "거래" }, "transactionCancelAttempted": { - "message": "$2에서 Gas 요금이 $1인 거래 취소 시도됨" + "message": "$2에서 Gas 수수료가 $1인 거래의 취소가 시도되었습니다." }, "transactionCancelSuccess": { "message": "$2에서 거래 취소 성공" }, "transactionConfirmed": { - "message": "$2에서의 거래가 확인되었습니다." + "message": "$2에서 거래가 확인되었습니다." }, "transactionCreated": { - "message": "$2에서 $1 값으로 거래를 만들었습니다." + "message": "$2에서 $1 값으로 거래가 생성되었습니다." }, "transactionDropped": { "message": "$2에서의 거래가 중단되었습니다." @@ -2230,10 +2290,10 @@ "message": "거래 수수료" }, "transactionResubmitted": { - "message": "$2에서 Gas 요금이 $1(으)로 증가한 거래가 다시 제출됨" + "message": "$2에서 Gas 수수료가 $1(으)로 증가한 거래가 다시 제출되었습니다." }, "transactionSubmitted": { - "message": "$2에서 Gas 요금이 $1인 거래가 제출되었습니다." + "message": "$2에서 Gas 수수료가 $1인 거래가 제출되었습니다." }, "transactionUpdated": { "message": "$2에서의 거래가 업데이트되었습니다." @@ -2242,17 +2302,17 @@ "message": "전송" }, "transferBetweenAccounts": { - "message": "계정 간 전송" + "message": "내 계정 간 전송" }, "transferFrom": { "message": "전송 위치" }, "troubleConnectingToWallet": { - "message": "$1 연결 도중 문제가 발생했습니다. $2을(를) 검도하고 다시 시도해 보세요.", + "message": "$1 연결 도중 문제가 발생했습니다. $2을(를) 검토하고 다시 시도해 보세요.", "description": "$1 is the wallet device name; $2 is a link to wallet connection guide" }, "troubleTokenBalances": { - "message": "토큰 잔액을 로드하는 도중 문제가 발생했습니다. 잔액을 확인할 수 있습니다. ", + "message": "토큰 잔액을 로드하는 도중 문제가 발생했습니다. 다음에서 잔액을 확인하세요. ", "description": "Followed by a link (here) to view token balances" }, "trustSiteApprovePermission": { @@ -2310,7 +2370,7 @@ "message": "URI에는 적절한 HTTP/HTTPS 접두사가 필요합니다." }, "urlExistsErrorMsg": { - "message": "URL이 기존 네트워크 목록에 이미 존재함" + "message": "이 URL은 현재 $1 네트워크에서 사용됩니다." }, "usePhishingDetection": { "message": "피싱 감지 사용" @@ -2332,6 +2392,10 @@ "message": "$1에서 이 토큰 확인", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "$1에서 이 토큰이 거래하려는 토큰이 맞는지 확인하세요.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "계정 보기" }, @@ -2360,10 +2424,14 @@ "message": "당사의 하드웨어 지갑 연결 가이드" }, "walletSeed": { - "message": "계정 시드 구문" + "message": "비밀 복구 구문" }, "walletSeedRestore": { - "message": "지갑 계정 시드 구문" + "message": "지갑 비밀 복구 구문" + }, + "web3ShimUsageNotification": { + "message": "현재의 웹사이트가 제거된 window.web3 API를 이용하려고 합니다. 이 사이트가 제대로 작동하지 않는 경우, $1을(를) 클릭해 자세히 알아보세요.", + "description": "$1 is a clickable link." }, "welcome": { "message": "MetaMask 방문을 환영합니다" @@ -2382,11 +2450,11 @@ "message": "메모지에 이 구문을 적어 안전한 곳에 보관하세요. 보안을 더욱 강화하고 싶다면 여러 메모지에 적은 다음 2~3곳에 보관하세요." }, "xOfY": { - "message": "$2 중 $1", + "message": "$1/$2개", "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" }, "xOfYPending": { - "message": "$2개 중 $1개 보류 중", + "message": "$1/$2개 보류 중", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" }, "yesLetsTry": { @@ -2399,9 +2467,9 @@ "message": "서명 중입니다." }, "yourPrivateSeedPhrase": { - "message": "비밀 백업 구문 확인" + "message": "비공개 비밀 복구 구문" }, "zeroGasPriceOnSpeedUpError": { - "message": "가속화 시 가스 가격 0" + "message": "가속화 시 Gas 가격 0" } } diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 833ccc493..da2c253b0 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Jūsų naršyklė neatpažįstama..." }, - "builtInCalifornia": { - "message": "„MetaMask“ suprojektuota ir įdiegta Kalifornijoje." - }, "buyWithWyre": { "message": "Pirkti ETH su „Wyre“" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 0ad89609f..2aa8e03e4 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Jūsu pārlūkprogramma netiek atbalstīta..." }, - "builtInCalifornia": { - "message": "MetaMask ir izstrādāta un izveidota Kalifornijā." - }, "buyWithWyre": { "message": "Pirkt ETH ar Wyre" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 1f5438c72..7ad59e94f 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Pelayar anda tidak disokong..." }, - "builtInCalifornia": { - "message": "MetaMask direka dan dibina di California." - }, "buyWithWyre": { "message": "Beli ETH dengan Wyre" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index aaaac5ffc..e70f716af 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -40,9 +40,6 @@ "blockiesIdenticon": { "message": "Gebruik Blockies Identicon" }, - "builtInCalifornia": { - "message": "MetaMask is ontworpen en gebouwd in Californië." - }, "cancel": { "message": "Annuleer" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index ff6fd6bfd..1728b607c 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Nettleseren din støttes ikke ..." }, - "builtInCalifornia": { - "message": "MetaMask ble bygget og designet i California." - }, "buyWithWyre": { "message": "Kjøp ETH med Wyre" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index f67d67d4e..56763f94d 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -6,7 +6,7 @@ "message": "Bersyon, support center, at impormasyon sa pakikipag-ugnayan" }, "acceleratingATransaction": { - "message": "* Kapag in-accelerate ang transaksyon sa pamamagitan ng paggamit ng mas mataas na presyo ng gas, mas magiging malaki ang tsansang mas mabilis na maproseso ng network, pero hindi ito palaging ginagarantiya." + "message": "* Kapag in-accelerate ang transaksyon sa pamamagitan ng paggamit ng mas mataas na presyo ng gas, mas magiging malaki ang tsansang mas mabilis na maiproseso ng network, pero hindi ito palaging ginagarantiya." }, "acceptTermsOfUse": { "message": "Nabasa ko at sumasang-ayon ako sa $1", @@ -52,6 +52,10 @@ "addContact": { "message": "Magdagdag ng contact" }, + "addCustomTokenByContractAddress": { + "message": "Walang makitang token? Puwede kang manual na magdagdag ng anumang token sa pamamagitan ng pag-paste ng address nito. Makikita ang mga address ng kontrata ng token sa $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Bibigyang-daan nito na magamit ang network na ito sa MetaMask." }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "Hindi sinusuportahan ang iyong Browser..." }, - "builContactList": { + "buildContactList": { "message": "Buuin ang iyong listahan ng contact" }, - "builtInCalifornia": { - "message": "Ang MetaMask ay idinisenyo at binuo sa California." - }, "buy": { "message": "Bumili" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "Ginagamit ang chain ID para maglagda ng mga transaksyon para sa network na ito." }, + "chainIdExistsErrorMsg": { + "message": "Kasalukuyang ginagamit ng $1 network ang Chain ID na ito." + }, "chromeRequiredForHardwareWallets": { "message": "Kailangan mong gamitin ang MetaMask sa Google Chrome para maikonekta sa iyong Hardware Wallet." }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Magpatuloy sa Wyre" }, + "contract": { + "message": "Kontrata" + }, "contractAddressError": { "message": "Magpapadala ka ng mga token sa address ng kontrata ng token. Posible itong magresulta sa pagkawala ng mga token na ito." }, @@ -641,7 +648,7 @@ "description": "$1 is the return value of eth_chainId from an RPC endpoint" }, "ensNotFoundOnCurrentNetwork": { - "message": "Hindi nahanapa ang ENS name sa kasalukuyang network. Subukang lumipat sa Ethereum Mainnet." + "message": "Hindi nahanap ang ENS name sa kasalukuyang network. Subukang lumipat sa Ethereum Mainnet." }, "ensRegistrationError": { "message": "Nagka-error sa pag-register ng ENS name" @@ -694,7 +701,7 @@ "message": "Mga Tinatantyang Tagal ng Pagproseso" }, "ethGasPriceFetchWarning": { - "message": "Ibinibigay ang backup na presyo ng gas dahil hindi available ang pangunahing serbisyo sa pagtatantiya ng gas sa ngayon." + "message": "Ibinibigay ang backup na presyo ng gas dahil hindi available ang pangunahing serbisyo sa pagtatantya ng gas sa ngayon." }, "eth_accounts": { "message": "Tingnan ang mga address ng iyong mga pinapayagang account (kinakailangan)", @@ -793,7 +800,7 @@ "message": "Sobrang Baba ng Presyo ng Gas" }, "gasPriceFetchFailed": { - "message": "Hindi nagtagumpay ang pagtatantiya ng presyo ng gas dahil sa error sa network." + "message": "Hindi nagtagumpay ang pagtatantya ng presyo ng gas dahil sa error sa network." }, "gasPriceInfoTooltipContent": { "message": "Tinutukoy ng presyo ng gas ang halaga ng Ether na handa mong bayaran para sa bawat unit ng gas." @@ -893,6 +900,12 @@ "message": "o $1", "description": "$1 represents the text from `importAccountLinkText` as a link" }, + "importTokenQuestion": { + "message": "Mag-import ng token?" + }, + "importTokenWarning": { + "message": "Sinuman ay makakagawa ng token na may anumang pangalan, kasama ang mga pekeng bersyon ng mga token na mayroon na. Magdagdag at mag-trade sa sarili mong pananagutan!" + }, "importWallet": { "message": "Mag-import ng wallet" }, @@ -1022,7 +1035,7 @@ "message": "Mga Link" }, "loadMore": { - "message": "Matuto Pa" + "message": "Mag-load Pa" }, "loading": { "message": "Nilo-load..." @@ -1127,7 +1140,7 @@ "message": "Pangalan" }, "needEtherInWallet": { - "message": "Para makaugnayan ang mga decentralized ma application gamit ang MetaMask, kakailanganin mo ang Ether sa iyong wallet." + "message": "Para makaugnayan ang mga decentralized na application gamit ang MetaMask, kakailanganin mo ang Ether sa iyong wallet." }, "needHelp": { "message": "Kailangan ng tulong? Makipag-ugnayan sa $1", @@ -1162,7 +1175,7 @@ "message": "Testnet" }, "networkSettingsChainIdDescription": { - "message": "Ginagaamit ang chain ID sa paglagda ng mga transaksyon. Dapat itong tumugma sa chain ID na ibinalik ng network. Puwede kang maglagay ng decimal o '0x'-prefixed hexadecimal number, pero ipapakita namin ang numero sa decimal." + "message": "Ginagamit ang chain ID sa paglagda ng mga transaksyon. Dapat itong tumugma sa chain ID na ibinalik ng network. Puwede kang maglagay ng decimal o '0x'-prefixed hexadecimal number, pero ipapakita namin ang numero sa decimal." }, "networkSettingsDescription": { "message": "Magdagdag at mag-edit ng mga custom na RPC network" @@ -1214,7 +1227,7 @@ "message": "Susunod" }, "nextNonceWarning": { - "message": "Mas mataas ang noncesa iminumungkahing nonce na $1", + "message": "Mas mataas ang nonce sa iminumungkahing nonce na $1", "description": "The next nonce according to MetaMask's internal logic" }, "noAccountsFound": { @@ -1295,6 +1308,22 @@ "message": "Tinatawag na ngayong \"Secret Recovery Phrase\" mo ang iyong \"Seed Phrase.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Simula sa Chrome version 91, hindi na susuportahan ng API na nag-enable sa aming Ledger support (U2F) ang mga hardware wallet. Nagpatupad ang MetaMask ng bagong Ledger Live support na nagbibigay-daan sa iyong patuloy na ikonekta ang Ledger device mo sa pamamagitan ng Ledger Live desktop app.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Kapag ginagamit ang iyong Ledger account sa MetaMask, may bagong tab na magbubukas at hihilingin sa iyong buksan ang Ledger Live app. Kapag nagbukas ang app, hihilingin sa iyong payagan ang isang koneksyon ng WebSocket sa MetaMask account mo. Iyon lang!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Puwede mong i-enable ang Ledger Live support sa pamamagitan ng pag-click sa Mga Setting > Advanced > Gamitin ang Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Update sa Ledger Support para sa Mga Chrome User", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "ng" }, @@ -1422,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Maghanap, pampublikong address (0x), o ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Magsimula rito" + }, + "recoveryPhraseReminderConfirm": { + "message": "OK" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Palaging panatilihin ang iyong Secret Recovery Phrase sa isang ligtas at lihim na lugar" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Kailangan ulit i-back up ang Secret Recovery Phrase mo?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Huwag kailanman ipaalam sa iba ang iyong Secret Recovery Phrase" + }, + "recoveryPhraseReminderItemTwo": { + "message": "Hindi kailanman hihingin ng MetaMask team ang iyong Secret Recovery Phrase" + }, + "recoveryPhraseReminderSubText": { + "message": "Kinokontrol ng iyong Secret Recovery Phrase ang lahat ng iyong account." + }, + "recoveryPhraseReminderTitle": { + "message": "Protektahan ang iyong pondo!" + }, "reject": { "message": "Tanggihan" }, @@ -1866,7 +1919,7 @@ "message": "Ito ay pagtatantya ng bayarin sa network na gagamitin para kumpletuhin ang iyong pag-swap. Posibleng magbago ang aktuwal na halaga ayon sa mga kundisyon ng network." }, "swapFailedErrorDescriptionWithSupportLink": { - "message": "May mga hindi pagtatagumpay sa transkasyon na nangyayari at narito kami para tumulong. Kung magpapatuloy ang isyung ito, puwede kang makipag-ugnayan sa aming suporta sa customer sa $1 para sa karagdagang tulong.", + "message": "May mga hindi pagtatagumpay sa transaksyon na nangyayari at narito kami para tumulong. Kung magpapatuloy ang isyung ito, puwede kang makipag-ugnayan sa aming suporta sa customer sa $1 para sa karagdagang tulong.", "description": "This message is shown to a user if their swap fails. The $1 will be replaced by support.metamask.io" }, "swapFailedErrorTitle": { @@ -1898,7 +1951,7 @@ "message": "Posibleng hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." }, "swapMaxNetworkFeeInfo": { - "message": "Aang “$1” ay ang pinakamalaking gagastusin mo. Kapag volatile ang network, maaaring malaking halaga ito.", + "message": "“$1” ang pinakamalaking gagastusin mo. Kapag volatile ang network, maaaring malaking halaga ito.", "description": "$1 will be the translation of swapMaxNetworkFees, with the font bolded" }, "swapMaxNetworkFees": { @@ -2047,6 +2100,9 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Manual na idinagdag ang token na ito." + }, "swapTokenVerificationMessage": { "message": "Palaging kumpirmahin ang address ng token sa $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2314,7 +2370,7 @@ "message": "Kinakailangan ng mga URL ang naaangkop na HTTP/HTTPS prefix." }, "urlExistsErrorMsg": { - "message": "Nasa kasalukuyang listahan ng mga network na ang URL" + "message": "Kasalukuyang ginagamit ng $1 network ang URL na ito." }, "usePhishingDetection": { "message": "Gumamit ng Pag-detect ng Phishing" @@ -2336,6 +2392,10 @@ "message": "I-verify ang token na ito sa $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "I-verify ang token na ito sa $1 at tiyaking ito ang token na gusto mong i-trade.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Tingnan ang Account" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 075940604..209b01e35 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Twoja przeglądarka nie jest obsługiwana..." }, - "builtInCalifornia": { - "message": "MetaMask został zaprojektowany i stworzony w Kaliforni." - }, "buyWithWyre": { "message": "Kup ETH poprzez Wyre" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e72e564df..1d0676ee1 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -43,9 +43,6 @@ "blockiesIdenticon": { "message": "Usar Blockies Identicon" }, - "builtInCalifornia": { - "message": "MetaMask é desenhada e construída na California." - }, "cancel": { "message": "Cancelar" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 369298586..117c704f8 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "Adicionar contato" }, + "addCustomTokenByContractAddress": { + "message": "Não conseguiu encontrar um token? Cole o endereço para adicionar manualmente qualquer token. Os endereços de contato do token podem ser encontrados em $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Isso permitirá esta rede ser usada dentro do MetaMask." }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "Seu navegador não é compatível..." }, - "builContactList": { + "buildContactList": { "message": "Crie sua lista de contatos" }, - "builtInCalifornia": { - "message": "O MetaMask é projetado e construído na Califórnia." - }, "buy": { "message": "Comprar" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "O ID da chain usado para assinar transações para essa rede." }, + "chainIdExistsErrorMsg": { + "message": "O ID da chain é usado no momento pela rede $1." + }, "chromeRequiredForHardwareWallets": { "message": "Você precisa usar MetaMask no Google Chrome para se conectar com sua carteira de hardware." }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Continuar para o Wyre" }, + "contract": { + "message": "Contrato" + }, "contractAddressError": { "message": "Você está enviando tokens ao endereço de contrato do token. Isso pode resultar na perda destes tokens." }, @@ -626,6 +633,10 @@ "endOfFlowMessage6": { "message": "Se você precisar fazer backup da sua Frase de recuperação secreta novamente, encontre-a em Configurações -> Segurança." }, + "endOfFlowMessage7": { + "message": "Se você tiver alguma pergunta ou vir algo suspeito, entre em contato com o atendimento ao cliente em $1.", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, "endOfFlowMessage8": { "message": "O MetaMask não pode recuperar sua Frase de recuperação secreta." }, @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "Importe uma conta com a Frase de recuperação secreta" }, + "importAccountText": { + "message": "ou $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "Importar token?" + }, + "importTokenWarning": { + "message": "Qualquer pessoa pode criar um token com um nome, incluindo versões falsas de tokens existentes. Adicione e negocie, assumindo o risco sozinho!" + }, "importWallet": { "message": "Importar carteira" }, @@ -961,6 +982,12 @@ "invalidSeedPhrase": { "message": "Frase de recuperação secreta inválida" }, + "ipfsGateway": { + "message": "Gateway IPFS" + }, + "ipfsGatewayDescription": { + "message": "Informe o URL do gateway de CID do IPFS para usar com resolução de conteúdo de ENS." + }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -1104,7 +1131,7 @@ "message": "Informe sua senha para confirmar que é você mesmo!" }, "mustSelectOne": { - "message": "Selecione pelo menos 1 token." + "message": "Selecione pelo menos 1 token." }, "myAccounts": { "message": "Minhas contas" @@ -1281,8 +1308,24 @@ "message": "A sua \"Frase Semente\" agora é chamada de sua \"Frase Secreta de Recuperação.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "A partir do Chrome versão 91, a API que permitia nosso suporte ao Ledger (U2F) não é mais compatível com carteiras de hardware. O MetaMask implementou um novo suporte ao Ledger Live que permite continuar conectando o seu dispositivo Ledger device por meio do aplicativo de desktop Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Ao interagir com sua conta do Ledger no MetaMask, uma nova aba será aberta e você deverá abrir o aplicativo Ledger Live. Quando o aplicativo for aberto, você precisará permitir uma conexão do WebSocket com sua conta do MetaMask. É isso!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Você pode habilitar o suporte do Ledger Live clicando em Configurações > Avançadas > Usar Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Atualização de suporte do Ledger para usuários do Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { - "message": " de " + "message": "de" }, "off": { "message": "Desativado" @@ -1408,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Busca, endereço público (0x) ou ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Iniciar aqui" + }, + "recoveryPhraseReminderConfirm": { + "message": "Entendi" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Mantenha sempre o sigilo e proteja a sua Frase de Recuperação Secreta." + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Precisa fazer backup da sua Frase de recuperação Secreta novamente?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Nunca compartilhe a sua Frase de recuperação secreta com ninguém" + }, + "recoveryPhraseReminderItemTwo": { + "message": "A equipe do MetaMask jamais pedirá sua Frase de recuperação secreta." + }, + "recoveryPhraseReminderSubText": { + "message": "Sua Frase de recuperação secreta controla todas as suas contas." + }, + "recoveryPhraseReminderTitle": { + "message": "Proteja seu dinheiro" + }, "reject": { "message": "Rejeitar" }, @@ -1456,6 +1523,16 @@ "restoreAccountWithSeed": { "message": "Restaure sua conta com a Frase de recuperação secreta" }, + "restoreWalletPreferences": { + "message": "Encontramos um backup dos seus dados de $1. Gostaria de restaurar as preferências da sua carteira?", + "description": "$1 is the date at which the data was backed up" + }, + "retryTransaction": { + "message": "Refazer transação" + }, + "reusedTokenNameWarning": { + "message": "O token aqui reutiliza um símbolo de outro token que você observa; isso pode causar confusões ou induzir ao erro." + }, "revealSeedWords": { "message": "Revelar Frase de recuperação secreta" }, @@ -1525,6 +1602,42 @@ "securitySettingsDescription": { "message": "Configurações de privacidade e Frase de recuperação secreta" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Anote e guarde em vários locais secretos." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "Salve em um gerenciador de senhas" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "Guarde dentro de um cofre." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "Guarde em um cofre-forte bancário." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "A sua frase de recuperação é a “chave-mestra” para sua carteira e seus fundos." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "Caso alguém lhe peça a sua frase de recuperação, essa pessoa provavelmente está tentando dar um golpe em você." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Jamais compartilhe a sua frase de recuperação, mesmo com o MetaMask!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "O que é uma frase de recuperação?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "Devo compartilhar minha frase de recuperação?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "Como salvo minha frase de recuperação?" + }, + "seedPhraseIntroTitle": { + "message": "Proteger sua carteira" + }, + "seedPhraseIntroTitleCopy": { + "message": "Antes de iniciar, assista esse vídeo curto para aprender sobre sua frase de recuperação e sobre como manter sua carteira segura." + }, "seedPhrasePlaceholder": { "message": "Separe cada palavra com um único espaço" }, @@ -1987,6 +2100,9 @@ "message": "Swap $1 para $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Este token foi adicionado manualmente." + }, "swapTokenVerificationMessage": { "message": "Sempre confirme o endereço do token em $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2254,7 +2370,7 @@ "message": "Os URLs precisam do prefixo HTTP/HTTPS adequado." }, "urlExistsErrorMsg": { - "message": "O URL já está presente na lista de redes existente" + "message": "O ID da chain é usado no momento pela rede $1." }, "usePhishingDetection": { "message": "Usar detecção de phishing" @@ -2276,6 +2392,10 @@ "message": "Verificar este token em $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Verifique este token em $1 garanta que seja o token que você deseja negociar.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Exibir conta" }, @@ -2309,6 +2429,10 @@ "walletSeedRestore": { "message": "Frase de recuperação secreta da carteira" }, + "web3ShimUsageNotification": { + "message": "Percebemos que o site atual tentou usar a API window.web3 removida. Se o site parecer estar corrompido, clique em $1 para obter mais informações.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Bem-vindo(a) ao MetaMask" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index d18b032df..2ba1a3a60 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Browserul dvs. nu este compatibil..." }, - "builtInCalifornia": { - "message": "MetaMask este concepută și creată în California." - }, "buyWithWyre": { "message": "Cumpărați ETH cu Wyre" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 8c2870a49..233d76216 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "Добавить контакт" }, + "addCustomTokenByContractAddress": { + "message": "Невозможно найти токен? Вы можете вручную добавить любой токен, вставив его адрес. Контактные адреса токена можно найти на $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Это позволит использовать ее в MetaMask." }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "Ваш браузер не поддерживается..." }, - "builContactList": { + "buildContactList": { "message": "Создайте список контактов" }, - "builtInCalifornia": { - "message": "MetaMask разработан и построен в Калифорнии." - }, "buy": { "message": "Купить" }, @@ -280,11 +281,14 @@ "message": "Отменено" }, "chainId": { - "message": "Идентификатор цепи" + "message": "Идентификатор цепочки" }, "chainIdDefinition": { "message": "Идентификатор цепочки, используемый для подписания транзакций для этой сети." }, + "chainIdExistsErrorMsg": { + "message": "Этот идентификатор цепочки в настоящее время используется сетью $1." + }, "chromeRequiredForHardwareWallets": { "message": "Вам необходимо использовать MetaMask в Google Chrome, чтобы подключиться к аппаратному кошельку." }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Продолжить к Wyre" }, + "contract": { + "message": "Контракт" + }, "contractAddressError": { "message": "Вы отправляете токены на адрес контракта токена. Это может привести к потере токенов." }, @@ -626,6 +633,10 @@ "endOfFlowMessage6": { "message": "Если вам нужно снова создать резервную копию секретной фразы восстановления, вы можете найти ее в Настройки -> Безопасность." }, + "endOfFlowMessage7": { + "message": "Если у вас возникнут вопросы или вы увидите что-то подозрительное, обратитесь в службу поддержки $1.", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, "endOfFlowMessage8": { "message": "Просто помните, что MetaMask не может восстановить секретную фразу восстановления." }, @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "Импортировать счет с секретной фразой восстановления" }, + "importAccountText": { + "message": "или $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "Импортировать токен?" + }, + "importTokenWarning": { + "message": "Кто угодно может создать токен с любым именем, включая поддельные версии существующих токенов. Добавляйте и торгуйте на свой страх и риск!" + }, "importWallet": { "message": "Импортировать кошелек" }, @@ -1287,6 +1308,22 @@ "message": "Исходная фраза теперь называется секретной фразой восстановления.", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Начиная с Chrome версии 91, API, обеспечивающий поддержку нашего Ledger (U2F), аппаратные кошельки больше не поддерживаются. MetaMask реализовала новую поддержку Ledger Live, которая позволяет продолжать подключаться к устройству Ledger через настольное приложение Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "При взаимодействии с вашим счетом Ledger в MetaMask откроется новая вкладка, и вам будет предложено открыть приложение Ledger Live. Когда приложение откроется, вам будет предложено разрешить WebSocket-соединение с вашим счетом MetaMask. Вот и все!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Вы можете включить поддержку Ledger Live, нажав «Настройки» > «Дополнительно» > «Использовать Ledger Live».", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Обновление поддержки Ledger для пользователей Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "из" }, @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Поиск, публичный адрес (0x) или ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Начать здесь" + }, + "recoveryPhraseReminderConfirm": { + "message": "Понятно" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Всегда храните свою секретную фразу восстановления в надежном и секретном месте" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Нужно снова сделать резервную копию секретной фразы восстановления?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Никогда никому не сообщайте свою секретную фразу восстановления" + }, + "recoveryPhraseReminderItemTwo": { + "message": "Команда MetaMask никогда неожиданно не запросит вашу секретную фразу восстановления" + }, + "recoveryPhraseReminderSubText": { + "message": "Ваша секретная фраза восстановления контролирует все ваши счета." + }, + "recoveryPhraseReminderTitle": { + "message": "Защитите свои активы" + }, "reject": { "message": "Отклонить" }, @@ -1473,7 +1534,7 @@ "message": "Токен здесь повторно использует символ из другого токена, который вы смотрите, это может запутать или ввести в заблуждение." }, "revealSeedWords": { - "message": "Показать секретную фразу восстановления" + "message": "Раскрыть секретную фразу восстановления" }, "revealSeedWordsDescription": { "message": "Если вы меняете браузер или переходите на другой компьютер, вам понадобится эта секретная фраза восстановления для доступа к своим счетам. Сохраните ее в безопасном секретном месте." @@ -1541,6 +1602,42 @@ "securitySettingsDescription": { "message": "Настройки конфиденциальности и секретная фраза восстановления кошелька" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Запишите и храните в нескольких секретных местах." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "В диспетчере паролей." + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "В банковской ячейке." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "В банковском сейфе." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "Фраза восстановления — это главный ключ к кошельку и средствам в нем." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "Если кто-нибудь интересуется вашей фразой восстановления, этот человек, скорее всего, пытается вас обмануть." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Не сообщайте свою фразу восстановления никому, даже сотрудникам MetaMask." + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "Что такое фраза восстановления?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "Можно ли сообщать кому-либо свою фразу восстановления?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "Как хранить фразу восстановления?" + }, + "seedPhraseIntroTitle": { + "message": "Защитите свой кошелек" + }, + "seedPhraseIntroTitleCopy": { + "message": "Прежде чем приступить к работе, посмотрите это короткое видео о том, что такое фраза восстановления и как обезопасить кошелек." + }, "seedPhrasePlaceholder": { "message": "Отделяйте каждое слово одним пробелом" }, @@ -2003,6 +2100,9 @@ "message": "Своп $1 на $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Этот токен был добавлен вручную." + }, "swapTokenVerificationMessage": { "message": "Всегда проверяйте адрес токена на $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2270,7 +2370,7 @@ "message": "Для URL требуется соответствующий префикс HTTP/HTTPS." }, "urlExistsErrorMsg": { - "message": "URL уже присутствует в имеющемся списке сетей" + "message": "Это URL в настоящее время используется сетью $1." }, "usePhishingDetection": { "message": "Использовать обнаружение фишинга" @@ -2292,6 +2392,10 @@ "message": "Проверить этот токен на $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Проверьте этот токен на $1 и убедитесь, что это тот токен, которым вы хотите торговать.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Посмотреть счет" }, @@ -2325,6 +2429,10 @@ "walletSeedRestore": { "message": "Секретная фраза восстановления кошелька" }, + "web3ShimUsageNotification": { + "message": "Мы заметили, что текущий веб-сайт пытался использовать удаленный API window.web3. Если сайт не работает, нажмите $1 для получения дополнительной информации.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Добро пожаловать в MetaMask" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index c8be9c255..2cf89a50b 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -137,9 +137,6 @@ "browserNotSupported": { "message": "Váš prehliadač nie je podporovaný..." }, - "builtInCalifornia": { - "message": "MetaMask je navržen a vytvořen v Kalifornii." - }, "buyWithWyre": { "message": "Kúpte ETH s Wyre" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index d1779de49..d4de42b88 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Vaš brskalnik ni podptrt ..." }, - "builtInCalifornia": { - "message": "MetaMask je zasnovan in ustvarjen v Kaliforniji." - }, "buyWithWyre": { "message": "Kupi ETH z Wyre" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 50780aa35..8170b146a 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Vaš pregledač nije podržan..." }, - "builtInCalifornia": { - "message": "MetaMask je dizajniran i izgrađen u Kaliforniji." - }, "buyWithWyre": { "message": "Kupite ETH preko servisa Wyre" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 4ce66b4e6..8fbe73469 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Din webbläsare stöds inte..." }, - "builtInCalifornia": { - "message": "MetaMask är skapat och utformat i Kalifornien." - }, "buyWithWyre": { "message": "Köp ETH med Wyre" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 541a39f90..5e493a99f 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -140,9 +140,6 @@ "browserNotSupported": { "message": "Kivinjari chaku hakiwezeshwi..." }, - "builtInCalifornia": { - "message": "MetaMask imeundwa na kutengenezwa California." - }, "buyWithWyre": { "message": "Nunua ETH kwa kutumia Wyre" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 4f6ab5c14..425b104c1 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -55,9 +55,6 @@ "blockiesIdenticon": { "message": "ப்ளாக்கிஸ் ஐடென்டிகோன் பயன்பாட்டு" }, - "builtInCalifornia": { - "message": "மேடமஸ்க் வடிவமைக்கப்பட்டு கலிபோர்னியாவில் கட்டப்பட்டுள்ளது." - }, "cancel": { "message": "ரத்து செய்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 0f8e7cdeb..3c193a838 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -49,9 +49,6 @@ "blockiesIdenticon": { "message": "ใช้งาน Blockies Identicon" }, - "builtInCalifornia": { - "message": "MetaMask ออกแบบและพัฒนาที่แคลิฟอร์เนีย" - }, "cancel": { "message": "ยกเลิก" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index cffc323a2..af413eadd 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -211,9 +211,6 @@ "browserNotSupported": { "message": "Hindi sinusuportahan ang iyong Browser..." }, - "builtInCalifornia": { - "message": "Ang MetaMask ay idinisenyo at binuo sa California." - }, "buy": { "message": "Bilhin" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 1bb278b72..3c4bdebf7 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -46,9 +46,6 @@ "blockiesIdenticon": { "message": "Blockies Identicon kullan" }, - "builtInCalifornia": { - "message": "MetaMask California'da tasarlandı ve yaratıldı" - }, "cancel": { "message": "Vazgeç" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 5b6fe762e..290378224 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -143,9 +143,6 @@ "browserNotSupported": { "message": "Ваш браузер не підтримується..." }, - "builtInCalifornia": { - "message": "MetaMask розроблено й створено в Каліфорнії." - }, "buyWithWyre": { "message": "Купити ETH через Wyre" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index dd91e2af6..ca3ca9c66 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "Thêm người liên hệ" }, + "addCustomTokenByContractAddress": { + "message": "Bạn không tìm thấy token? Bạn có thể dán địa chỉ của bất kỳ token nào để thêm token đó theo cách thủ công. Bạn có thể tìm thấy địa chỉ hợp đồng token trên $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "Thao tác này sẽ cho phép sử dụng mạng này trong MetaMask." }, @@ -249,12 +253,9 @@ "browserNotSupported": { "message": "Trình duyệt của bạn không được hỗ trợ..." }, - "builContactList": { + "buildContactList": { "message": "Xây dựng danh sách liên hệ của bạn" }, - "builtInCalifornia": { - "message": "MetaMask được thiết kế và phát triển tại California." - }, "buy": { "message": "Mua" }, @@ -285,6 +286,9 @@ "chainIdDefinition": { "message": "Mã chuỗi được dùng để ký các giao dịch cho mạng này." }, + "chainIdExistsErrorMsg": { + "message": "Mạng $1 hiện đang sử dụng mã chuỗi này." + }, "chromeRequiredForHardwareWallets": { "message": "Bạn cần sử dụng MetaMask trên Google Chrome để kết nối với Ví cứng của bạn." }, @@ -410,6 +414,9 @@ "continueToWyre": { "message": "Tiếp tục chuyển đến Wyre" }, + "contract": { + "message": "Hợp đồng" + }, "contractAddressError": { "message": "Bạn đang gửi token đến địa chỉ hợp đồng của token. Điều này có thể khiến bạn bị mất những token này." }, @@ -624,7 +631,11 @@ "message": "Hãy cẩn thận với hoạt động lừa đảo! MetaMask sẽ không bao giờ tự ý hỏi Cụm mật khẩu khôi phục bí mật của bạn." }, "endOfFlowMessage6": { - "message": "Nếu bạn cần sao lưu lại Cụm mật khẩu khôi phục bí mật, bạn có thể tìm thấy chức năng này trong Cài đặt -> Bảo mật." + "message": "Nếu bạn cần sao lưu lại Cụm mật khẩu khôi phục bí mật, bạn có thể tìm thấy chức năng này trong phần Cài đặt -> Bảo mật." + }, + "endOfFlowMessage7": { + "message": "Nếu bạn có thắc mắc hoặc thấy điều gì đó đáng ngờ, hãy liên hệ với bộ phận hỗ trợ của chúng tôi $1.", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." }, "endOfFlowMessage8": { "message": "MetaMask không thể khôi phục Cụm mật khẩu khôi phục bí mật của bạn." @@ -885,6 +896,16 @@ "importAccountSeedPhrase": { "message": "Nhập một tài khoản bằng Cụm mật khẩu khôi phục bí mật" }, + "importAccountText": { + "message": "hoặc $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" + }, + "importTokenQuestion": { + "message": "Bạn muốn nhập token?" + }, + "importTokenWarning": { + "message": "Bất kỳ ai cũng tạo được token bằng bất kỳ tên nào, kể cả phiên bản giả của token hiện có. Bạn tự chịu rủi ro khi thêm và giao dịch!" + }, "importWallet": { "message": "Nhập ví" }, @@ -1287,6 +1308,22 @@ "message": "Từ giờ, \"Cụm mật khẩu gốc\" sẽ được gọi là \"Cụm mật khẩu khôi phục bí mật.\"", "description": "Description of a notification in the 'See What's New' popup. Describes the seed phrase wording update." }, + "notifications6DescriptionOne": { + "message": "Kể từ phiên bản Chrome 91, API từng cho phép hỗ trợ Ledger (U2F) của chúng tôi không còn hỗ trợ ví cứng nữa. MetaMask đã triển khai một tính năng hỗ trợ Ledger Live mới cho phép bạn tiếp tục kết nối với thiết bị Ledger của mình thông qua ứng dụng Ledger Live trên máy tính.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionThree": { + "message": "Khi tương tác với tài khoản Ledger của bạn trong MetaMask, một tab mới sẽ mở ra và bạn sẽ được yêu cầu mở ứng dụng Ledger Live. Khi ứng dụng này mở ra, bạn sẽ được yêu cầu cho phép kết nối WebSocket với tài khoản MetaMask của mình. Đơn giản vậy thôi!", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6DescriptionTwo": { + "message": "Bạn có thể kích hoạt tính năng hỗ trợ Ledger Live bằng cách nhấp vào phần Cài đặt > Nâng cao > Sử dụng Ledger Live.", + "description": "Description of a notification in the 'See What's New' popup. Describes the Ledger support update." + }, + "notifications6Title": { + "message": "Thông tin cập nhật về việc hỗ trợ Ledger cho người dùng Chrome", + "description": "Title for a notification in the 'See What's New' popup. Lets users know about the Ledger support update" + }, "ofTextNofM": { "message": "trên" }, @@ -1414,6 +1451,30 @@ "recipientAddressPlaceholder": { "message": "Tìm kiếm, địa chỉ công khai (0x) hoặc ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Bắt đầu tại đây" + }, + "recoveryPhraseReminderConfirm": { + "message": "Đã hiểu" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Luôn lưu giữ Cụm mật khẩu khôi phục bí mật ở nơi an toàn và bí mật" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Bạn cần sao lưu lại Cụm mật khẩu khôi phục bí mật?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Tuyệt đối không cho ai biết Cụm mật khẩu khôi phục bí mật" + }, + "recoveryPhraseReminderItemTwo": { + "message": "Nhóm MetaMask sẽ không bao giờ hỏi Cụm mật khẩu khôi phục bí mật của bạn" + }, + "recoveryPhraseReminderSubText": { + "message": "Cụm mật khẩu khôi phục bí mật sẽ kiểm soát mọi thứ trong tài khoản của bạn." + }, + "recoveryPhraseReminderTitle": { + "message": "Bảo vệ tiền của bạn" + }, "reject": { "message": "Từ chối" }, @@ -1439,7 +1500,7 @@ "message": "Xóa tài khoản" }, "removeAccountDescription": { - "message": "Tài khoản này sẽ được xóa khỏi ví của bạn. Xin đảm bảo rằng bạn có Cụm mật khẩu khôi phục bí mật ban đầu hoặc khóa riêng tư cho tài khoản được nhập trước khi tiếp tục. Bạn có thể nhập hoặc tạo lại tài khoản từ trình đơn tài khoản thả xuống. " + "message": "Tài khoản này sẽ được xóa khỏi ví của bạn. Hãy đảm bảo rằng bạn có Cụm mật khẩu khôi phục bí mật ban đầu hoặc khóa riêng tư cho tài khoản được nhập trước khi tiếp tục. Bạn có thể nhập hoặc tạo lại tài khoản từ trình đơn tài khoản thả xuống. " }, "requestsAwaitingAcknowledgement": { "message": "yêu cầu đang chờ xác nhận" @@ -1473,10 +1534,10 @@ "message": "Một token trong đây sử dụng lại ký hiệu của một token khác mà bạn thấy, điều này có thể gây nhầm lẫn hoặc mang tính lừa dối." }, "revealSeedWords": { - "message": "Hiện cụm mật khẩu khôi phục bí mật" + "message": "Hiện Cụm mật khẩu khôi phục bí mật" }, "revealSeedWordsDescription": { - "message": "Nếu thay đổi trình duyệt hoặc chuyển máy tính, bạn sẽ cần Cụm mật khẩu khôi phục bí mật này để truy cập tài khoản của mình. Hãy lưu cụm mật khẩu gốc này ở nơi an toàn và bí mật." + "message": "Nếu thay đổi trình duyệt hoặc chuyển máy tính, bạn sẽ cần Cụm mật khẩu khôi phục bí mật này để truy cập tài khoản của mình. Hãy lưu Cụm mật khẩu khôi phục bí mật này ở nơi an toàn và bí mật." }, "revealSeedWordsTitle": { "message": "Cụm mật khẩu khôi phục bí mật" @@ -1541,6 +1602,42 @@ "securitySettingsDescription": { "message": "Các cài đặt quyền riêng tư và Cụm mật khẩu khôi phục bí mật của ví" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Viết ra và cất ở nhiều nơi bí mật." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "Lưu trong một trình quản lý mật khẩu" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "Lưu giữ trong hộp ký gửi an toàn." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "Lưu giữ trong két an toàn." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "Cụm mật khẩu khôi phục bí mật là “chìa khóa chính” để truy cập ví và số tiền của bạn." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "Nếu ai đó hỏi bạn cụm mật khẩu khôi phục bí mật, thì họ đang cố gắng lừa đảo bạn." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Đừng bao giờ cho ai biết cụm mật khẩu khôi phục bí mật, kể cả MetaMask!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "Cụm mật khẩu khôi phục là gì?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "Tôi có nên cho ai biết cụm mật khẩu khôi phục bí mật của mình không?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "Tôi lưu cụm mật khẩu khôi phục của mình bằng cách nào?" + }, + "seedPhraseIntroTitle": { + "message": "Bảo mật cho ví của bạn" + }, + "seedPhraseIntroTitleCopy": { + "message": "Trước khi bắt đầu, hãy xem video ngắn này để tìm hiểu thêm về cụm mật khẩu khôi phục bí mật của bạn và cách bảo vệ ví của bạn." + }, "seedPhrasePlaceholder": { "message": "Phân tách mỗi từ bằng một dấu cách" }, @@ -1841,7 +1938,7 @@ "message": "Đang hoàn tất..." }, "swapFromTo": { - "message": "Hoán đổi $1 sang $2", + "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" }, "swapGasFeesSplit": { @@ -2003,6 +2100,9 @@ "message": "Hoán đổi $1 sang $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "Token này đã được thêm theo cách thủ công." + }, "swapTokenVerificationMessage": { "message": "Luôn xác nhận địa chỉ token trên $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." @@ -2270,7 +2370,7 @@ "message": "URL phải có tiền tố HTTP/HTTPS phù hợp." }, "urlExistsErrorMsg": { - "message": "URL đã có trong danh sách mạng hiện tại" + "message": "Mạng $1 hiện đang sử dụng URL này." }, "usePhishingDetection": { "message": "Sử dụng tính năng Phát hiện lừa đảo" @@ -2292,6 +2392,10 @@ "message": "Xác minh token này trên $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Hãy xác minh token này trên $1 và đảm bảo đây là token bạn muốn giao dịch.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "Xem tài khoản" }, @@ -2325,6 +2429,10 @@ "walletSeedRestore": { "message": "Cụm mật khẩu khôi phục bí mật của ví" }, + "web3ShimUsageNotification": { + "message": "Chúng tôi nhận thấy rằng trang web hiện tại đã cố dùng API window.web3 đã bị xóa. Nếu trang web có vẻ như đã bị lỗi, vui lòng nhấp vào $1 để biết thêm thông tin.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Chào mừng bạn đến với MetaMask" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 969150137..3eb07cf4e 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -217,9 +217,6 @@ "browserNotSupported": { "message": "您的浏览器不支持该功能……" }, - "builtInCalifornia": { - "message": "MetaMask在加利福尼亚设计和制造。" - }, "buy": { "message": "购买" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 5756be928..245558800 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -149,9 +149,6 @@ "browserNotSupported": { "message": "您的瀏覽器尚未支援..." }, - "builtInCalifornia": { - "message": "MetaMask 是在加州設計製造" - }, "buy": { "message": "買" }, diff --git a/app/background.html b/app/background.html index 290576939..2faa31411 100644 --- a/app/background.html +++ b/app/background.html @@ -5,11 +5,13 @@ - - - - - + + + + + {{@each(it.jsBundles) => val}} + + {{/each}} diff --git a/app/home.html b/app/home.html index d952983c9..5350b31dd 100644 --- a/app/home.html +++ b/app/home.html @@ -11,10 +11,12 @@
- - - - - + + + + + {{@each(it.jsBundles) => val}} + + {{/each}} diff --git a/app/images/camera.svg b/app/images/camera.svg deleted file mode 100644 index 4d3f73ec6..000000000 --- a/app/images/camera.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/images/connect-white.svg b/app/images/connect-white.svg deleted file mode 100644 index e9063ae46..000000000 --- a/app/images/connect-white.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/copy.svg b/app/images/copy.svg deleted file mode 100644 index 9ee2317b7..000000000 --- a/app/images/copy.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/images/forward-carrat.svg b/app/images/forward-carrat.svg deleted file mode 100644 index c64355c78..000000000 --- a/app/images/forward-carrat.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/help.svg b/app/images/help.svg deleted file mode 100644 index 852663161..000000000 --- a/app/images/help.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/icons/blue-circle-info.svg b/app/images/icons/blue-circle-info.svg deleted file mode 100644 index ebed859d5..000000000 --- a/app/images/icons/blue-circle-info.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/icons/green-circle-check.svg b/app/images/icons/green-circle-check.svg deleted file mode 100644 index 305b326f7..000000000 --- a/app/images/icons/green-circle-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/icons/hamburger.svg b/app/images/icons/hamburger.svg deleted file mode 100644 index 64fc344ab..000000000 --- a/app/images/icons/hamburger.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/images/icons/info.svg b/app/images/icons/info.svg deleted file mode 100644 index dfb4ee049..000000000 --- a/app/images/icons/info.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/images/loginglogo.svg b/app/images/loginglogo.svg deleted file mode 100644 index e39001e10..000000000 --- a/app/images/loginglogo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/app/images/logo.png b/app/images/logo.png new file mode 100644 index 000000000..cd87ccb43 Binary files /dev/null and b/app/images/logo.png differ diff --git a/app/images/meta-shield.svg b/app/images/meta-shield.svg deleted file mode 100644 index f8eee0db1..000000000 --- a/app/images/meta-shield.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/mm-bolt.svg b/app/images/mm-bolt.svg deleted file mode 100644 index 8f2e3a7e5..000000000 --- a/app/images/mm-bolt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/mm-secure.svg b/app/images/mm-secure.svg deleted file mode 100644 index a85800751..000000000 --- a/app/images/mm-secure.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/images/open.svg b/app/images/open.svg deleted file mode 100644 index 07feb87b9..000000000 --- a/app/images/open.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/images/sleuth.svg b/app/images/sleuth.svg deleted file mode 100644 index 2ca1846d5..000000000 --- a/app/images/sleuth.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/images/source-logos-all.svg b/app/images/source-logos-all.svg deleted file mode 100644 index 22006c179..000000000 --- a/app/images/source-logos-all.svg +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/images/switch_acc.svg b/app/images/switch_acc.svg deleted file mode 100644 index 436e2e28a..000000000 --- a/app/images/switch_acc.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/images/transak.svg b/app/images/transak.svg new file mode 100644 index 000000000..8f8d7790a --- /dev/null +++ b/app/images/transak.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 437ef1d2e..6c328d0c6 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -33,8 +33,8 @@ "js": [ "disable-console.js", "globalthis.js", - "lockdown.js", - "runLockdown.js", + "lockdown-install.js", + "lockdown-run.js", "contentscript.js" ], "run_at": "document_start", diff --git a/app/notification.html b/app/notification.html index 8d82c41b2..55b98c960 100644 --- a/app/notification.html +++ b/app/notification.html @@ -34,10 +34,12 @@
- - - - - + + + + + {{@each(it.jsBundles) => val}} + + {{/each}} diff --git a/app/phishing.html b/app/phishing.html index 7bbc11dab..5460447b9 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -3,8 +3,8 @@ Ethereum Phishing Detection - MetaMask - - + + diff --git a/app/popup.html b/app/popup.html index d16257992..c94b82df8 100644 --- a/app/popup.html +++ b/app/popup.html @@ -11,10 +11,12 @@
- - - - - + + + + + {{@each(it.jsBundles) => val}} + + {{/each}} diff --git a/app/scripts/constants/on-ramp.js b/app/scripts/constants/on-ramp.js new file mode 100644 index 000000000..335d7a9ad --- /dev/null +++ b/app/scripts/constants/on-ramp.js @@ -0,0 +1 @@ +export const TRANSAK_API_KEY = '25ac1309-a49b-4411-b20e-5e56c61a5b1c'; // It's a public key, which will be included in a URL for Transak. diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index 4d4578124..7bd788271 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -30,6 +30,9 @@ describe('DetectTokensController', function () { '0x7e57e2', '0xbc86727e770de68b1060c91f6bb6945c73e10388', ]); + sandbox + .stub(network, 'getLatestBlock') + .callsFake(() => Promise.resolve({})); sandbox .stub(preferences, '_detectIsERC721') .returns(Promise.resolve(false)); diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index e5fc03543..4d13532fc 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -34,8 +34,10 @@ const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); * @typedef {Object} EtherscanTransaction * @property {string} blockNumber - The number of the block this transaction was found in, in decimal * @property {string} from - The hex-prefixed address of the sender - * @property {string} gas - The gas limit, in decimal WEI - * @property {string} gasPrice - The gas price, in decimal WEI + * @property {string} gas - The gas limit, in decimal GWEI + * @property {string} [gasPrice] - The gas price, in decimal WEI + * @property {string} [maxFeePerGas] - The maximum fee per gas, inclusive of tip, in decimal WEI + * @property {string} [maxPriorityFeePerGas] - The maximum tip per gas in decimal WEI * @property {string} hash - The hex-prefixed transaction hash * @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed) * @property {string} nonce - The transaction nonce, in decimal @@ -267,6 +269,25 @@ export default class IncomingTransactionsController { etherscanTransaction.isError === '0' ? TRANSACTION_STATUSES.CONFIRMED : TRANSACTION_STATUSES.FAILED; + const txParams = { + from: etherscanTransaction.from, + gas: bnToHex(new BN(etherscanTransaction.gas)), + nonce: bnToHex(new BN(etherscanTransaction.nonce)), + to: etherscanTransaction.to, + value: bnToHex(new BN(etherscanTransaction.value)), + }; + + if (etherscanTransaction.gasPrice) { + txParams.gasPrice = bnToHex(new BN(etherscanTransaction.gasPrice)); + } else if (etherscanTransaction.maxFeePerGas) { + txParams.maxFeePerGas = bnToHex( + new BN(etherscanTransaction.maxFeePerGas), + ); + txParams.maxPriorityFeePerGas = bnToHex( + new BN(etherscanTransaction.maxPriorityFeePerGas), + ); + } + return { blockNumber: etherscanTransaction.blockNumber, id: createId(), @@ -274,14 +295,7 @@ export default class IncomingTransactionsController { metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId], status, time, - txParams: { - from: etherscanTransaction.from, - gas: bnToHex(new BN(etherscanTransaction.gas)), - gasPrice: bnToHex(new BN(etherscanTransaction.gasPrice)), - nonce: bnToHex(new BN(etherscanTransaction.nonce)), - to: etherscanTransaction.to, - value: bnToHex(new BN(etherscanTransaction.value)), - }, + txParams, hash: etherscanTransaction.hash, type: TRANSACTION_TYPES.INCOMING, }; diff --git a/app/scripts/controllers/incoming-transactions.test.js b/app/scripts/controllers/incoming-transactions.test.js index f4cac0309..70b511ace 100644 --- a/app/scripts/controllers/incoming-transactions.test.js +++ b/app/scripts/controllers/incoming-transactions.test.js @@ -103,15 +103,34 @@ function getMockBlockTracker() { /** * Returns a transaction object matching the expected format returned * by the Etherscan API - * - * @param {string} [toAddress] - The hex-prefixed address of the recipient - * @param {number} [blockNumber] - The block number for the transaction + * @param {Object} [params] - options bag + * @param {string} [params.toAddress] - The hex-prefixed address of the recipient + * @param {number} [params.blockNumber] - The block number for the transaction + * @param {boolean} [params.useEIP1559] - Use EIP-1559 gas fields + * @param * @returns {EtherscanTransaction} */ -const getFakeEtherscanTransaction = ( +const getFakeEtherscanTransaction = ({ toAddress = MOCK_SELECTED_ADDRESS, blockNumber = 10, -) => { + useEIP1559 = false, + hash = '0xfake', +} = {}) => { + if (useEIP1559) { + return { + blockNumber: blockNumber.toString(), + from: '0xfake', + gas: '0', + maxFeePerGas: '10', + maxPriorityFeePerGas: '1', + hash, + isError: '0', + nonce: '100', + timeStamp: '16000000000000', + to: toAddress, + value: '0', + }; + } return { blockNumber: blockNumber.toString(), from: '0xfake', @@ -243,7 +262,13 @@ describe('IncomingTransactionsController', function () { 200, JSON.stringify({ status: '1', - result: [getFakeEtherscanTransaction()], + result: [ + getFakeEtherscanTransaction(), + getFakeEtherscanTransaction({ + hash: '0xfakeeip1559', + useEIP1559: true, + }), + ], }), ); const updateStateStub = sinon.stub( @@ -263,6 +288,9 @@ describe('IncomingTransactionsController', function () { const actualStateWithoutGenerated = cloneDeep(actualState); delete actualStateWithoutGenerated?.incomingTransactions?.['0xfake']?.id; + delete actualStateWithoutGenerated?.incomingTransactions?.[ + '0xfakeeip1559' + ]?.id; assert.ok( typeof generatedTxId === 'number' && generatedTxId > 0, @@ -290,6 +318,24 @@ describe('IncomingTransactionsController', function () { value: '0x0', }, }, + '0xfakeeip1559': { + blockNumber: '10', + hash: '0xfakeeip1559', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, + status: TRANSACTION_STATUSES.CONFIRMED, + time: 16000000000000000, + type: TRANSACTION_TYPES.INCOMING, + txParams: { + from: '0xfake', + gas: '0x0', + maxFeePerGas: '0xa', + maxPriorityFeePerGas: '0x1', + nonce: '0x64', + to: '0x0101', + value: '0x0', + }, + }, }, incomingTxLastFetchedBlockByChainId: { ...getNonEmptyInitState().incomingTxLastFetchedBlockByChainId, @@ -509,7 +555,11 @@ describe('IncomingTransactionsController', function () { 200, JSON.stringify({ status: '1', - result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)], + result: [ + getFakeEtherscanTransaction({ + toAddress: NEW_MOCK_SELECTED_ADDRESS, + }), + ], }), ); const updateStateStub = sinon.stub( @@ -586,7 +636,9 @@ describe('IncomingTransactionsController', function () { // reply with a valid request for any supported network, so that this test has every opportunity to fail nockEtherscanApiForAllChains({ status: '1', - result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)], + result: [ + getFakeEtherscanTransaction({ toAddress: NEW_MOCK_SELECTED_ADDRESS }), + ], }); const updateStateStub = sinon.stub( incomingTransactionsController.store, @@ -954,7 +1006,9 @@ describe('IncomingTransactionsController', function () { describe('_getNewIncomingTransactions', function () { const ADDRESS_TO_FETCH_FOR = '0xfakeaddress'; - const FETCHED_TX = getFakeEtherscanTransaction(ADDRESS_TO_FETCH_FOR); + const FETCHED_TX = getFakeEtherscanTransaction({ + toAddress: ADDRESS_TO_FETCH_FOR, + }); const mockFetch = sinon.stub().returns( Promise.resolve({ json: () => Promise.resolve({ status: '1', result: [FETCHED_TX] }), @@ -1212,5 +1266,53 @@ describe('IncomingTransactionsController', function () { type: TRANSACTION_TYPES.INCOMING, }); }); + + it('should return the expected data when the tx uses EIP-1559 fields', function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + blockTracker: getMockBlockTracker(), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), + preferencesController: getMockPreferencesController(), + initState: getNonEmptyInitState(), + }, + ); + + const result = incomingTransactionsController._normalizeTxFromEtherscan( + { + timeStamp: '4444', + isError: '0', + blockNumber: 333, + from: '0xa', + gas: '11', + maxFeePerGas: '12', + maxPriorityFeePerGas: '1', + nonce: '13', + to: '0xe', + value: '15', + hash: '0xg', + }, + ROPSTEN_CHAIN_ID, + ); + + assert.deepStrictEqual(result, { + blockNumber: 333, + id: 54321, + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, + status: TRANSACTION_STATUSES.CONFIRMED, + time: 4444000, + txParams: { + from: '0xa', + gas: '0xb', + maxFeePerGas: '0xc', + maxPriorityFeePerGas: '0x1', + nonce: '0xd', + to: '0xe', + value: '0xf', + }, + hash: '0xg', + type: TRANSACTION_TYPES.INCOMING, + }); + }); }); }); diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index eb298f37d..772a9ebde 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -1,8 +1,8 @@ import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import createBlockReRefMiddleware from 'eth-json-rpc-middleware/block-ref'; +import createBlockRefMiddleware from 'eth-json-rpc-middleware/block-ref'; import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty'; import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'; -import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'; +import createInflightCacheMiddleware from 'eth-json-rpc-middleware/inflight-cache'; import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'; import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'; import createInfuraMiddleware from 'eth-json-rpc-infura'; @@ -23,8 +23,8 @@ export default function createInfuraClient({ network, projectId }) { const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), createBlockCacheMiddleware({ blockTracker }), - createInflightMiddleware(), - createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: infuraProvider }), createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), createBlockTrackerInspectorMiddleware({ blockTracker }), infuraMiddleware, diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index 3417241f6..6c234e852 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -1,11 +1,13 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; import { getNetworkDisplayName } from './util'; -import NetworkController from './network'; +import NetworkController, { NETWORK_EVENTS } from './network'; describe('NetworkController', function () { describe('controller', function () { let networkController; + let getLatestBlockStub; + let setProviderTypeAndWait; const noop = () => undefined; const networkControllerProviderConfig = { getAccounts: noop, @@ -13,7 +15,21 @@ describe('NetworkController', function () { beforeEach(function () { networkController = new NetworkController(); + getLatestBlockStub = sinon + .stub(networkController, 'getLatestBlock') + .callsFake(() => Promise.resolve({})); networkController.setInfuraProjectId('foo'); + setProviderTypeAndWait = () => + new Promise((resolve) => { + networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { + resolve(); + }); + networkController.setProviderType('mainnet'); + }); + }); + + afterEach(function () { + getLatestBlockStub.reset(); }); describe('#provider', function () { @@ -67,6 +83,59 @@ describe('NetworkController', function () { ); }); }); + + describe('#getEIP1559Compatibility', function () { + it('should return false when baseFeePerGas is not in the block header', async function () { + networkController.initializeProvider(networkControllerProviderConfig); + const supportsEIP1559 = await networkController.getEIP1559Compatibility(); + assert.equal(supportsEIP1559, false); + }); + + it('should return true when baseFeePerGas is in block header', async function () { + networkController.initializeProvider(networkControllerProviderConfig); + getLatestBlockStub.callsFake(() => + Promise.resolve({ baseFeePerGas: '0xa ' }), + ); + const supportsEIP1559 = await networkController.getEIP1559Compatibility(); + assert.equal(supportsEIP1559, true); + }); + + it('should store EIP1559 support in state to reduce calls to getLatestBlock', async function () { + networkController.initializeProvider(networkControllerProviderConfig); + getLatestBlockStub.callsFake(() => + Promise.resolve({ baseFeePerGas: '0xa ' }), + ); + await networkController.getEIP1559Compatibility(); + const supportsEIP1559 = await networkController.getEIP1559Compatibility(); + assert.equal(getLatestBlockStub.calledOnce, true); + assert.equal(supportsEIP1559, true); + }); + + it('should clear stored EIP1559 support when changing networks', async function () { + networkController.initializeProvider(networkControllerProviderConfig); + networkController.consoleThis = true; + getLatestBlockStub.callsFake(() => + Promise.resolve({ baseFeePerGas: '0xa ' }), + ); + await networkController.getEIP1559Compatibility(); + assert.equal( + networkController.networkDetails.getState().EIPS[1559], + true, + ); + getLatestBlockStub.callsFake(() => Promise.resolve({})); + await setProviderTypeAndWait('mainnet'); + assert.equal( + networkController.networkDetails.getState().EIPS[1559], + undefined, + ); + await networkController.getEIP1559Compatibility(); + assert.equal( + networkController.networkDetails.getState().EIPS[1559], + false, + ); + assert.equal(getLatestBlockStub.calledTwice, true); + }); + }); }); describe('utils', function () { diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 1799f658f..6d9ca795d 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -51,6 +51,10 @@ const defaultProviderConfig = { ...defaultProviderConfigOpts, }; +const defaultNetworkDetailsState = { + EIPS: { 1559: undefined }, +}; + export const NETWORK_EVENTS = { // Fired after the actively selected network is changed NETWORK_DID_CHANGE: 'networkDidChange', @@ -74,10 +78,21 @@ export default class NetworkController extends EventEmitter { this.providerStore.getState(), ); this.networkStore = new ObservableStore('loading'); + // We need to keep track of a few details about the current network + // Ideally we'd merge this.networkStore with this new store, but doing so + // will require a decent sized refactor of how we're accessing network + // state. Currently this is only used for detecting EIP 1559 support but + // can be extended to track other network details. + this.networkDetails = new ObservableStore( + opts.networkDetails || { + ...defaultNetworkDetailsState, + }, + ); this.store = new ComposedStore({ provider: this.providerStore, previousProviderStore: this.previousProviderStore, network: this.networkStore, + networkDetails: this.networkDetails, }); // provider and block tracker @@ -120,6 +135,46 @@ export default class NetworkController extends EventEmitter { return { provider, blockTracker }; } + /** + * Method to return the latest block for the current network + * @returns {Object} Block header + */ + getLatestBlock() { + return new Promise((resolve, reject) => { + const { provider } = this.getProviderAndBlockTracker(); + const ethQuery = new EthQuery(provider); + ethQuery.sendAsync( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (err, block) => { + if (err) { + return reject(err); + } + return resolve(block); + }, + ); + }); + } + + /** + * Method to check if the block header contains fields that indicate EIP 1559 + * support (baseFeePerGas). + * @returns {Promise} true if current network supports EIP 1559 + */ + async getEIP1559Compatibility() { + const { EIPS } = this.networkDetails.getState(); + if (process.env.SHOW_EIP_1559_UI === false) { + return false; + } + if (EIPS[1559] !== undefined) { + return EIPS[1559]; + } + const latestBlock = await this.getLatestBlock(); + const supportsEIP1559 = + latestBlock && latestBlock.baseFeePerGas !== undefined; + this.setNetworkEIPSupport(1559, supportsEIP1559); + return supportsEIP1559; + } + verifyNetwork() { // Check network when restoring connectivity: if (this.isNetworkLoading()) { @@ -135,6 +190,26 @@ export default class NetworkController extends EventEmitter { this.networkStore.putState(network); } + /** + * Set EIP support indication in the networkDetails store + * @param {number} EIPNumber - The number of the EIP to mark support for + * @param {boolean} isSupported - True if the EIP is supported + */ + setNetworkEIPSupport(EIPNumber, isSupported) { + this.networkDetails.updateState({ + EIPS: { + [EIPNumber]: isSupported, + }, + }); + } + + /** + * Reset EIP support to default (no support) + */ + clearNetworkDetails() { + this.networkDetails.putState({ ...defaultNetworkDetailsState }); + } + isNetworkLoading() { return this.getNetworkState() === 'loading'; } @@ -154,6 +229,8 @@ export default class NetworkController extends EventEmitter { 'NetworkController - lookupNetwork aborted due to missing chainId', ); this.setNetworkState('loading'); + // keep network details in sync with network state + this.clearNetworkDetails(); return; } @@ -174,10 +251,14 @@ export default class NetworkController extends EventEmitter { if (initialNetwork === currentNetwork) { if (err) { this.setNetworkState('loading'); + // keep network details in sync with network state + this.clearNetworkDetails(); return; } this.setNetworkState(networkVersion); + // look up EIP-1559 support + this.getEIP1559Compatibility(); } }); } @@ -298,9 +379,15 @@ export default class NetworkController extends EventEmitter { } _switchNetwork(opts) { + // Indicate to subscribers that network is about to change this.emit(NETWORK_EVENTS.NETWORK_WILL_CHANGE); + // Set loading state this.setNetworkState('loading'); + // Reset network details + this.clearNetworkDetails(); + // Configure the provider appropriately this._configureProvider(opts); + // Notify subscribers that network has changed this.emit(NETWORK_EVENTS.NETWORK_DID_CHANGE, opts.type); } diff --git a/app/scripts/controllers/network/pending-middleware.test.js b/app/scripts/controllers/network/pending-middleware.test.js index 9d89e59f1..ca97063b7 100644 --- a/app/scripts/controllers/network/pending-middleware.test.js +++ b/app/scripts/controllers/network/pending-middleware.test.js @@ -53,6 +53,7 @@ describe('PendingNonceMiddleware', function () { getPendingTransactionByHash, }); const spec = { + accessList: null, blockHash: null, blockNumber: null, from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', @@ -62,6 +63,7 @@ describe('PendingNonceMiddleware', function () { '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', input: '0x', nonce: '0x4', + type: '0x0', to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', transactionIndex: null, value: '0x0', diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index 7f55f2521..6e61a9e01 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -1,24 +1,48 @@ import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network'; +import { TRANSACTION_ENVELOPE_TYPES } from '../../../../shared/constants/transaction'; export const getNetworkDisplayName = (key) => NETWORK_TO_NAME_MAP[key]; export function formatTxMetaForRpcResult(txMeta) { - return { - blockHash: txMeta.txReceipt ? txMeta.txReceipt.blockHash : null, - blockNumber: txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null, - from: txMeta.txParams.from, - gas: txMeta.txParams.gas, - gasPrice: txMeta.txParams.gasPrice, - hash: txMeta.hash, - input: txMeta.txParams.data || '0x', - nonce: txMeta.txParams.nonce, - to: txMeta.txParams.to, - transactionIndex: txMeta.txReceipt - ? txMeta.txReceipt.transactionIndex - : null, - value: txMeta.txParams.value || '0x0', - v: txMeta.v, - r: txMeta.r, - s: txMeta.s, + const { r, s, v, hash, txReceipt, txParams } = txMeta; + const { + to, + data, + nonce, + gas, + from, + value, + gasPrice, + accessList, + maxFeePerGas, + maxPriorityFeePerGas, + } = txParams; + + const formattedTxMeta = { + v, + r, + s, + to, + gas, + from, + hash, + nonce, + input: data || '0x', + value: value || '0x0', + accessList: accessList || null, + blockHash: txReceipt?.blockHash || null, + blockNumber: txReceipt?.blockNumber || null, + transactionIndex: txReceipt?.transactionIndex || null, }; + + if (maxFeePerGas && maxPriorityFeePerGas) { + formattedTxMeta.maxFeePerGas = maxFeePerGas; + formattedTxMeta.maxPriorityFeePerGas = maxPriorityFeePerGas; + formattedTxMeta.type = TRANSACTION_ENVELOPE_TYPES.FEE_MARKET; + } else { + formattedTxMeta.gasPrice = gasPrice; + formattedTxMeta.type = TRANSACTION_ENVELOPE_TYPES.LEGACY; + } + + return formattedTxMeta; } diff --git a/app/scripts/controllers/network/util.test.js b/app/scripts/controllers/network/util.test.js new file mode 100644 index 000000000..fdf761886 --- /dev/null +++ b/app/scripts/controllers/network/util.test.js @@ -0,0 +1,99 @@ +import { strict as assert } from 'assert'; +import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; +import { formatTxMetaForRpcResult } from './util'; + +describe('network utils', function () { + describe('formatTxMetaForRpcResult', function () { + it('should correctly format the tx meta object (EIP-1559)', function () { + const txMeta = { + id: 1, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { + from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + maxFeePerGas: '0x77359400', + maxPriorityFeePerGas: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: 'sentEther', + origin: 'other', + chainId: '0x3', + time: 1624408066355, + metamaskNetworkId: '3', + hash: + '0x4bcb6cd6b182209585f8ad140260ddb35c81a575dd40f508d9767e652a9f60e7', + r: '0x4c3111e42ed5eec3dcecba1e234700f387e8693c373c61c3e54a762a26f1570e', + s: '0x18bfc4eeb7ebcfacc3bd59ea100a6834ea3265e65945dbec69aa2a06564fafff', + v: '0x29', + }; + const expectedResult = { + accessList: null, + blockHash: null, + blockNumber: null, + from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + gas: '0x7b0d', + hash: + '0x4bcb6cd6b182209585f8ad140260ddb35c81a575dd40f508d9767e652a9f60e7', + input: '0x', + maxFeePerGas: '0x77359400', + maxPriorityFeePerGas: '0x77359400', + nonce: '0x4b', + r: '0x4c3111e42ed5eec3dcecba1e234700f387e8693c373c61c3e54a762a26f1570e', + s: '0x18bfc4eeb7ebcfacc3bd59ea100a6834ea3265e65945dbec69aa2a06564fafff', + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + transactionIndex: null, + type: '0x2', + v: '0x29', + value: '0x0', + }; + const result = formatTxMetaForRpcResult(txMeta); + assert.deepEqual(result, expectedResult); + }); + + it('should correctly format the tx meta object (non EIP-1559)', function () { + const txMeta = { + id: 1, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { + from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: 'sentEther', + origin: 'other', + chainId: '0x3', + time: 1624408066355, + metamaskNetworkId: '3', + hash: + '0x4bcb6cd6b182209585f8ad140260ddb35c81a575dd40f508d9767e652a9f60e7', + r: '0x4c3111e42ed5eec3dcecba1e234700f387e8693c373c61c3e54a762a26f1570e', + s: '0x18bfc4eeb7ebcfacc3bd59ea100a6834ea3265e65945dbec69aa2a06564fafff', + v: '0x29', + }; + const expectedResult = { + accessList: null, + blockHash: null, + blockNumber: null, + from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + gas: '0x7b0d', + hash: + '0x4bcb6cd6b182209585f8ad140260ddb35c81a575dd40f508d9767e652a9f60e7', + input: '0x', + gasPrice: '0x77359400', + nonce: '0x4b', + r: '0x4c3111e42ed5eec3dcecba1e234700f387e8693c373c61c3e54a762a26f1570e', + s: '0x18bfc4eeb7ebcfacc3bd59ea100a6834ea3265e65945dbec69aa2a06564fafff', + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + transactionIndex: null, + type: '0x0', + v: '0x29', + value: '0x0', + }; + const result = formatTxMetaForRpcResult(txMeta); + assert.deepEqual(result, expectedResult); + }); + }); +}); diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 298272968..bd2c91a09 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -15,7 +15,7 @@ import { } from '../../../shared/modules/hexstring-utils'; import { NETWORK_EVENTS } from './network'; -const ERC721METADATA_INTERFACE_ID = '0x5b5e139f'; +const ERC721_INTERFACE_ID = '0x80ac58cd'; export default class PreferencesController { /** @@ -51,6 +51,7 @@ export default class PreferencesController { useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + useStaticTokenList: false, // WARNING: Do not use feature flags for security-sensitive things. // Feature flag toggling is available in the global namespace @@ -138,6 +139,16 @@ export default class PreferencesController { this.store.updateState({ usePhishDetect: val }); } + /** + * Setter for the `useStaticTokenList` property + * + * @param {boolean} val - Whether or not the user prefers to use the static token list or dynamic token list from the API + * + */ + setUseStaticTokenList(val) { + this.store.updateState({ useStaticTokenList: val }); + } + /** * Setter for the `firstTimeFlowType` property * @@ -803,7 +814,7 @@ export default class PreferencesController { ); return await tokenContract - .supportsInterface(ERC721METADATA_INTERFACE_ID) + .supportsInterface(ERC721_INTERFACE_ID) .catch((error) => { console.log('error', error); log.debug(error); diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index d5b993c8e..f088199ed 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -30,6 +30,9 @@ describe('preferences controller', function () { network.initializeProvider(networkControllerProviderConfig); provider = network.getProviderAndBlockTracker().provider; + sandbox + .stub(network, 'getLatestBlock') + .callsFake(() => Promise.resolve({})); sandbox.stub(network, 'getCurrentChainId').callsFake(() => currentChainId); sandbox .stub(network, 'getProviderConfig') @@ -866,4 +869,22 @@ describe('preferences controller', function () { ); }); }); + describe('setUseStaticTokenList', function () { + it('should default to false', function () { + const state = preferencesController.store.getState(); + assert.equal(state.useStaticTokenList, false); + }); + + it('should set the useStaticTokenList property in state', function () { + assert.equal( + preferencesController.store.getState().useStaticTokenList, + false, + ); + preferencesController.setUseStaticTokenList(true); + assert.equal( + preferencesController.store.getState().useStaticTokenList, + true, + ); + }); + }); }); diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index c69082bce..17425b678 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -6,7 +6,7 @@ import { mapValues, cloneDeep } from 'lodash'; import abi from 'human-standard-token-abi'; import { calcTokenAmount } from '../../../ui/helpers/utils/token-util'; import { calcGasTotal } from '../../../ui/pages/send/send.utils'; -import { conversionUtil } from '../../../ui/helpers/utils/conversion-util'; +import { conversionUtil } from '../../../shared/modules/conversion.utils'; import { DEFAULT_ERC20_APPROVE_GAS, QUOTES_EXPIRED_ERROR, diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 942fa5906..50e90f061 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,10 +1,11 @@ import EventEmitter from 'safe-event-emitter'; import { ObservableStore } from '@metamask/obs-store'; -import Transaction from 'ethereumjs-tx'; import { bufferToHex, keccak, toBuffer, isHexString } from 'ethereumjs-util'; import EthQuery from 'ethjs-query'; import { ethErrors } from 'eth-rpc-errors'; import abi from 'human-standard-token-abi'; +import Common from '@ethereumjs/common'; +import { TransactionFactory } from '@ethereumjs/tx'; import { ethers } from 'ethers'; import NonceTracker from 'nonce-tracker'; import log from 'loglevel'; @@ -23,10 +24,16 @@ import { hexWEIToDecGWEI } from '../../../../ui/helpers/utils/conversions.util'; import { TRANSACTION_STATUSES, TRANSACTION_TYPES, + TRANSACTION_ENVELOPE_TYPES, } from '../../../../shared/constants/transaction'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { GAS_LIMITS } from '../../../../shared/constants/gas'; -import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../../shared/constants/network'; +import { + HARDFORKS, + MAINNET, + NETWORK_TYPE_RPC, + CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, +} from '../../../../shared/constants/network'; import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; import TransactionStateManager from './tx-state-manager'; import TxGasUtil from './tx-gas-utils'; @@ -45,6 +52,16 @@ export const TRANSACTION_EVENTS = { SUBMITTED: 'Transaction Submitted', }; +/** + * @typedef {Object} CustomGasSettings + * @property {string} [gas] - The gas limit to use for the transaction + * @property {string} [gasPrice] - The gasPrice to use for a legacy transaction + * @property {string} [maxFeePerGas] - The maximum amount to pay per gas on a + * EIP-1559 transaction + * @property {string} [maxPriorityFeePerGas] - The maximum amount of paid fee + * to be distributed to miner in an EIP-1559 transaction + */ + /** Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller @@ -65,7 +82,7 @@ export const TRANSACTION_EVENTS = { @param {Object} opts.networkStore - an observable store for network number @param {Object} opts.blockTracker - An instance of eth-blocktracker @param {Object} opts.provider - A network provider. - @param {Function} opts.signTransaction - function the signs an ethereumjs-tx + @param {Function} opts.signTransaction - function the signs an @ethereumjs/tx @param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for @param {Function} opts.signTransaction - ethTx signer that returns a rawTx @param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state @@ -77,6 +94,8 @@ export default class TransactionController extends EventEmitter { super(); this.networkStore = opts.networkStore || new ObservableStore({}); this._getCurrentChainId = opts.getCurrentChainId; + this.getProviderConfig = opts.getProviderConfig; + this.getEIP1559Compatibility = opts.getEIP1559Compatibility; this.preferencesStore = opts.preferencesStore || new ObservableStore({}); this.provider = opts.provider; this.getPermittedAccounts = opts.getPermittedAccounts; @@ -158,6 +177,58 @@ export default class TransactionController extends EventEmitter { return integerChainId; } + /** + * @ethereumjs/tx uses @ethereumjs/common as a configuration tool for + * specifying which chain, network, hardfork and EIPs to support for + * a transaction. By referencing this configuration, and analyzing the fields + * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718 + * transaction type to use. + * @returns {Common} common configuration object + */ + async getCommonConfiguration() { + const { type, nickname: name } = this.getProviderConfig(); + const supportsEIP1559 = await this.getEIP1559Compatibility(); + + // This logic below will have to be updated each time a hardfork happens + // that carries with it a new Transaction type. It is inconsequential for + // hardforks that do not include new types. + const hardfork = supportsEIP1559 ? HARDFORKS.LONDON : HARDFORKS.BERLIN; + + // type will be one of our default network names or 'rpc'. the default + // network names are sufficient configuration, simply pass the name as the + // chain argument in the constructor. + if (type !== NETWORK_TYPE_RPC) { + return new Common({ + chain: type, + hardfork, + }); + } + + // For 'rpc' we need to use the same basic configuration as mainnet, + // since we only support EVM compatible chains, and then override the + // name, chainId and networkId properties. This is done using the + // `forCustomChain` static method on the Common class. + const chainId = parseInt(this._getCurrentChainId(), 16); + const networkId = this.networkStore.getState(); + + const customChainParams = { + name, + chainId, + // It is improbable for a transaction to be signed while the network + // is loading for two reasons. + // 1. Pending, unconfirmed transactions are wiped on network change + // 2. The UI is unusable (loading indicator) when network is loading. + // setting the networkId to 0 is for type safety and to explicity lead + // the transaction to failing if a user is able to get to this branch + // on a custom network that requires valid network id. I have not ran + // into this limitation on any network I have attempted, even when + // hardcoding networkId to 'loading'. + networkId: networkId === 'loading' ? 0 : parseInt(networkId, 10), + }; + + return Common.forCustomChain(MAINNET, customChainParams, hardfork); + } + /** Adds a tx to the txlist @emits ${txMeta.id}:unapproved @@ -251,6 +322,7 @@ export default class TransactionController extends EventEmitter { */ let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, + origin, }); if (origin === 'metamask') { @@ -274,8 +346,6 @@ export default class TransactionController extends EventEmitter { } } - txMeta.origin = origin; - const { type, getCodeResponse } = await this._determineTransactionType( txParams, ); @@ -329,7 +399,12 @@ export default class TransactionController extends EventEmitter { if (simulationFails) { txMeta.simulationFails = simulationFails; } - if (defaultGasPrice && !txMeta.txParams.gasPrice) { + if ( + defaultGasPrice && + !txMeta.txParams.gasPrice && + !txMeta.txParams.maxPriorityFeePerGas && + !txMeta.txParams.maxFeePerGas + ) { txMeta.txParams.gasPrice = defaultGasPrice; } if (defaultGasLimit && !txMeta.txParams.gas) { @@ -344,7 +419,10 @@ export default class TransactionController extends EventEmitter { * @returns {Promise} The default gas price */ async _getDefaultGasPrice(txMeta) { - if (txMeta.txParams.gasPrice) { + if ( + txMeta.txParams.gasPrice || + (txMeta.txParams.maxFeePerGas && txMeta.txParams.maxPriorityFeePerGas) + ) { return undefined; } const gasPrice = await this.query.gasPrice(); @@ -402,32 +480,105 @@ export default class TransactionController extends EventEmitter { return { gasLimit, simulationFails }; } + /** + * Given a TransactionMeta object, generate new gas params such that if the + * transaction was an EIP1559 transaction, it only has EIP1559 gas fields, + * otherwise it only has gasPrice. Will use whatever custom values are + * specified in customGasSettings, or falls back to incrementing by a percent + * which is defined by specifying a numerator. 11 is a 10% bump, 12 would be + * a 20% bump, and so on. + * @param {import( + * '../../../../shared/constants/transaction' + * ).TransactionMeta} originalTxMeta - Original transaction to use as base + * @param {CustomGasSettings} [customGasSettings] - overrides for the gas + * fields to use instead of the multiplier + * @param {number} [incrementNumerator] - Numerator from which to generate a + * percentage bump of gas price. E.g 11 would be a 10% bump over base. + * @returns {{ newGasParams: CustomGasSettings, previousGasParams: CustomGasSettings }} + */ + generateNewGasParams( + originalTxMeta, + customGasSettings = {}, + incrementNumerator = 11, + ) { + const { txParams } = originalTxMeta; + const previousGasParams = {}; + const newGasParams = {}; + if (customGasSettings.gasLimit) { + newGasParams.gas = customGasSettings?.gas ?? GAS_LIMITS.SIMPLE; + } + + if (isEIP1559Transaction(originalTxMeta)) { + previousGasParams.maxFeePerGas = txParams.maxFeePerGas; + previousGasParams.maxPriorityFeePerGas = txParams.maxPriorityFeePerGas; + newGasParams.maxFeePerGas = + customGasSettings?.maxFeePerGas || + bnToHex( + BnMultiplyByFraction( + hexToBn(txParams.maxFeePerGas), + incrementNumerator, + 10, + ), + ); + newGasParams.maxPriorityFeePerGas = + customGasSettings?.maxPriorityFeePerGas || + bnToHex( + BnMultiplyByFraction( + hexToBn(txParams.maxPriorityFeePerGas), + incrementNumerator, + 10, + ), + ); + } else { + previousGasParams.gasPrice = txParams.gasPrice; + newGasParams.gasPrice = + customGasSettings?.gasPrice || + bnToHex( + BnMultiplyByFraction( + hexToBn(txParams.gasPrice), + incrementNumerator, + 10, + ), + ); + } + + return { previousGasParams, newGasParams }; + } + /** * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to * the sender's address, and has a higher gasPrice than that of the previous transaction. * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel - * @param {string} [customGasPrice] - the hex value to use for the cancel transaction + * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas + * params instead of allowing this method to generate them * @returns {txMeta} */ - async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) { + async createCancelTransaction(originalTxId, customGasSettings) { const originalTxMeta = this.txStateManager.getTransaction(originalTxId); const { txParams } = originalTxMeta; - const { gasPrice: lastGasPrice, from, nonce } = txParams; + const { from, nonce } = txParams; + + const { previousGasParams, newGasParams } = this.generateNewGasParams( + originalTxMeta, + { + ...customGasSettings, + // We want to override the previous transactions gasLimit because it + // will now be a simple send instead of whatever it was before such + // as a token transfer or contract call. + gasLimit: customGasSettings.gasLimit || GAS_LIMITS.SIMPLE, + }, + ); - const newGasPrice = - customGasPrice || - bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10)); const newTxMeta = this.txStateManager.generateTxMeta({ txParams: { from, to: from, nonce, - gas: customGasLimit || GAS_LIMITS.SIMPLE, value: '0x0', - gasPrice: newGasPrice, + ...newGasParams, }, - lastGasPrice, + previousGasParams, loadingDefaults: false, status: TRANSACTION_STATUSES.APPROVED, type: TRANSACTION_TYPES.CANCEL, @@ -444,34 +595,30 @@ export default class TransactionController extends EventEmitter { * the same gas limit and a 10% higher gas price, though it is possible to set a custom value for * each instead. * @param {number} originalTxId - the id of the txMeta that you want to speed up - * @param {string} [customGasPrice] - The new custom gas price, in hex - * @param {string} [customGasLimit] - The new custom gas limt, in hex + * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas + * params instead of allowing this method to generate them * @returns {txMeta} */ - async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) { + async createSpeedUpTransaction(originalTxId, customGasSettings) { const originalTxMeta = this.txStateManager.getTransaction(originalTxId); const { txParams } = originalTxMeta; - const { gasPrice: lastGasPrice } = txParams; - const newGasPrice = - customGasPrice || - bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10)); + const { previousGasParams, newGasParams } = this.generateNewGasParams( + originalTxMeta, + customGasSettings, + ); const newTxMeta = this.txStateManager.generateTxMeta({ txParams: { ...txParams, - gasPrice: newGasPrice, + ...newGasParams, }, - lastGasPrice, + previousGasParams, loadingDefaults: false, status: TRANSACTION_STATUSES.APPROVED, type: TRANSACTION_TYPES.RETRY, }); - if (customGasLimit) { - newTxMeta.txParams.gas = customGasLimit; - } - this.addTransaction(newTxMeta); await this.approveTransaction(newTxMeta.id); return newTxMeta; @@ -530,9 +677,9 @@ export default class TransactionController extends EventEmitter { customNonceValue = Number(customNonceValue); nonceLock = await this.nonceTracker.getNonceLock(fromAddress); // add nonce to txParams - // if txMeta has lastGasPrice then it is a retry at same nonce with higher - // gas price transaction and their for the nonce should not be calculated - const nonce = txMeta.lastGasPrice + // if txMeta has previousGasParams then it is a retry at same nonce with + // higher gas settings and therefor the nonce should not be recalculated + const nonce = txMeta.previousGasParams ? txMeta.txParams.nonce : nonceLock.nextNonce; const customOrNonce = @@ -581,17 +728,26 @@ export default class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTransaction(txId); // add network/chain id const chainId = this.getChainId(); - const txParams = { ...txMeta.txParams, chainId }; + const type = isEIP1559Transaction(txMeta) + ? TRANSACTION_ENVELOPE_TYPES.FEE_MARKET + : TRANSACTION_ENVELOPE_TYPES.LEGACY; + const txParams = { + type, + ...txMeta.txParams, + chainId, + gasLimit: txMeta.txParams.gas, + }; // sign tx const fromAddress = txParams.from; - const ethTx = new Transaction(txParams); - await this.signEthTx(ethTx, fromAddress); + const common = await this.getCommonConfiguration(); + const unsignedEthTx = TransactionFactory.fromTxData(txParams, { common }); + const signedEthTx = await this.signEthTx(unsignedEthTx, fromAddress); // add r,s,v values for provider request purposes see createMetamaskMiddleware // and JSON rpc standard for further explanation - txMeta.r = bufferToHex(ethTx.r); - txMeta.s = bufferToHex(ethTx.s); - txMeta.v = bufferToHex(ethTx.v); + txMeta.r = bufferToHex(signedEthTx.r); + txMeta.s = bufferToHex(signedEthTx.s); + txMeta.v = bufferToHex(signedEthTx.v); this.txStateManager.updateTransaction( txMeta, @@ -600,7 +756,7 @@ export default class TransactionController extends EventEmitter { // set state to signed this.txStateManager.setTxStatusSigned(txMeta.id); - const rawTx = bufferToHex(ethTx.serialize()); + const rawTx = bufferToHex(signedEthTx.serialize()); return rawTx; } diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index b0cfa04f9..08ae84919 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import EventEmitter from 'events'; import { toBuffer } from 'ethereumjs-util'; -import EthTx from 'ethereumjs-tx'; +import { TransactionFactory } from '@ethereumjs/tx'; import { ObservableStore } from '@metamask/obs-store'; import sinon from 'sinon'; @@ -20,6 +20,9 @@ import TransactionController, { TRANSACTION_EVENTS } from '.'; const noop = () => true; const currentNetworkId = '42'; const currentChainId = '0x2a'; +const providerConfig = { + type: 'kovan', +}; const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; @@ -36,6 +39,7 @@ describe('Transaction Controller', function () { }; provider = createTestProviderTools({ scaffold: providerResultStub }) .provider; + fromAccount = getTestAccounts()[0]; const blockTrackerStub = new EventEmitter(); blockTrackerStub.getCurrentBlock = noop; @@ -46,13 +50,14 @@ describe('Transaction Controller', function () { return '0xee6b2800'; }, networkStore: new ObservableStore(currentNetworkId), + getEIP1559Compatibility: () => Promise.resolve(true), txHistoryLimit: 10, blockTracker: blockTrackerStub, signTransaction: (ethTx) => new Promise((resolve) => { - ethTx.sign(fromAccount.key); - resolve(); + resolve(ethTx.sign(fromAccount.key)); }), + getProviderConfig: () => providerConfig, getPermittedAccounts: () => undefined, getCurrentChainId: () => currentChainId, getParticipateInMetrics: () => false, @@ -565,8 +570,8 @@ describe('Transaction Controller', function () { noop, ); const rawTx = await txController.signTransaction('1'); - const ethTx = new EthTx(toBuffer(rawTx)); - assert.equal(ethTx.getChainId(), 42); + const ethTx = TransactionFactory.fromSerializedData(toBuffer(rawTx)); + assert.equal(ethTx.common.chainIdBN().toNumber(), 42); }); }); @@ -734,11 +739,11 @@ describe('Transaction Controller', function () { const addTransactionArgs = addTransactionSpy.getCall(0).args[0]; assert.deepEqual(addTransactionArgs.txParams, expectedTxParams); - const { lastGasPrice, type } = addTransactionArgs; + const { previousGasParams, type } = addTransactionArgs; assert.deepEqual( - { lastGasPrice, type }, + { gasPrice: previousGasParams.gasPrice, type }, { - lastGasPrice: '0xa', + gasPrice: '0xa', type: TRANSACTION_TYPES.RETRY, }, ); @@ -757,17 +762,70 @@ describe('Transaction Controller', function () { assert.deepEqual(result.txParams, expectedTxParams); - const { lastGasPrice, type } = result; + const { previousGasParams, type } = result; assert.deepEqual( - { lastGasPrice, type }, + { gasPrice: previousGasParams.gasPrice, type }, { - lastGasPrice: '0xa', + gasPrice: '0xa', type: TRANSACTION_TYPES.RETRY, }, ); }); }); + describe('#signTransaction', function () { + let fromTxDataSpy; + + beforeEach(function () { + fromTxDataSpy = sinon.spy(TransactionFactory, 'fromTxData'); + }); + + afterEach(function () { + fromTxDataSpy.restore(); + }); + + it('sets txParams.type to 0x0 (non-EIP-1559)', async function () { + txController.txStateManager._addTransactionsToState([ + { + status: TRANSACTION_STATUSES.UNAPPROVED, + id: 1, + metamaskNetworkId: currentNetworkId, + history: [{}], + txParams: { + from: VALID_ADDRESS_TWO, + to: VALID_ADDRESS, + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + }, + ]); + await txController.signTransaction('1'); + assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x0'); + }); + + it('sets txParams.type to 0x2 (EIP-1559)', async function () { + txController.txStateManager._addTransactionsToState([ + { + status: TRANSACTION_STATUSES.UNAPPROVED, + id: 2, + metamaskNetworkId: currentNetworkId, + history: [{}], + txParams: { + from: VALID_ADDRESS_TWO, + to: VALID_ADDRESS, + maxFeePerGas: '0x77359400', + maxPriorityFeePerGas: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + }, + ]); + await txController.signTransaction('2'); + assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x2'); + }); + }); + describe('#publishTransaction', function () { let hash, txMeta, trackTransactionMetricsEventSpy; diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index 804f43ec2..a31cac3a1 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -1,17 +1,23 @@ import { ethErrors } from 'eth-rpc-errors'; import { addHexPrefix } from '../../../lib/util'; -import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; +import { + TRANSACTION_ENVELOPE_TYPES, + TRANSACTION_STATUSES, +} from '../../../../../shared/constants/transaction'; import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils'; const normalizers = { - from: (from) => addHexPrefix(from), + from: addHexPrefix, to: (to, lowerCase) => lowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to), - nonce: (nonce) => addHexPrefix(nonce), - value: (value) => addHexPrefix(value), - data: (data) => addHexPrefix(data), - gas: (gas) => addHexPrefix(gas), - gasPrice: (gasPrice) => addHexPrefix(gasPrice), + nonce: addHexPrefix, + value: addHexPrefix, + data: addHexPrefix, + gas: addHexPrefix, + gasPrice: addHexPrefix, + maxFeePerGas: addHexPrefix, + maxPriorityFeePerGas: addHexPrefix, + type: addHexPrefix, }; export function normalizeAndValidateTxParams(txParams, lowerCase = true) { @@ -38,6 +44,78 @@ export function normalizeTxParams(txParams, lowerCase = true) { return normalizedTxParams; } +/** + * Given two fields, ensure that the second field is not included in txParams, + * and if it is throw an invalidParams error. + * @param {Object} txParams - the transaction parameters object + * @param {string} fieldBeingValidated - the current field being validated + * @param {string} mutuallyExclusiveField - the field to ensure is not provided + * @throws {ethErrors.rpc.invalidParams} - throws if mutuallyExclusiveField is + * present in txParams. + */ +function ensureMutuallyExclusiveFieldsNotProvided( + txParams, + fieldBeingValidated, + mutuallyExclusiveField, +) { + if (typeof txParams[mutuallyExclusiveField] !== 'undefined') { + throw ethErrors.rpc.invalidParams( + `Invalid transaction params: specified ${fieldBeingValidated} but also included ${mutuallyExclusiveField}, these cannot be mixed`, + ); + } +} + +/** + * Ensures that the provided value for field is a string, throws an + * invalidParams error if field is not a string. + * @param {Object} txParams - the transaction parameters object + * @param {string} field - the current field being validated + * @throws {ethErrors.rpc.invalidParams} - throws if field is not a string + */ +function ensureFieldIsString(txParams, field) { + if (typeof txParams[field] !== 'string') { + throw ethErrors.rpc.invalidParams( + `Invalid transaction params: ${field} is not a string. got: (${txParams[field]})`, + ); + } +} + +/** + * Ensures that the provided txParams has the proper 'type' specified for the + * given field, if it is provided. If types do not match throws an + * invalidParams error. + * @param {Object} txParams - the transaction parameters object + * @param {'gasPrice' | 'maxFeePerGas' | 'maxPriorityFeePerGas'} field - the + * current field being validated + * @throws {ethErrors.rpc.invalidParams} - throws if type does not match the + * expectations for provided field. + */ +function ensureProperTransactionEnvelopeTypeProvided(txParams, field) { + switch (field) { + case 'maxFeePerGas': + case 'maxPriorityFeePerGas': + if ( + txParams.type && + txParams.type !== TRANSACTION_ENVELOPE_TYPES.FEE_MARKET + ) { + throw ethErrors.rpc.invalidParams( + `Invalid transaction envelope type: specified type "${txParams.type}" but including maxFeePerGas and maxPriorityFeePerGas requires type: "${TRANSACTION_ENVELOPE_TYPES.FEE_MARKET}"`, + ); + } + break; + case 'gasPrice': + default: + if ( + txParams.type && + txParams.type === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET + ) { + throw ethErrors.rpc.invalidParams( + `Invalid transaction envelope type: specified type "${txParams.type}" but included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas`, + ); + } + } +} + /** * Validates the given tx parameters * @param {Object} txParams - the tx params @@ -64,12 +142,43 @@ export function validateTxParams(txParams) { case 'to': validateRecipient(txParams); break; + case 'gasPrice': + ensureProperTransactionEnvelopeTypeProvided(txParams, 'gasPrice'); + ensureMutuallyExclusiveFieldsNotProvided( + txParams, + 'gasPrice', + 'maxFeePerGas', + ); + ensureMutuallyExclusiveFieldsNotProvided( + txParams, + 'gasPrice', + 'maxPriorityFeePerGas', + ); + ensureFieldIsString(txParams, 'gasPrice'); + break; + case 'maxFeePerGas': + ensureProperTransactionEnvelopeTypeProvided(txParams, 'maxFeePerGas'); + ensureMutuallyExclusiveFieldsNotProvided( + txParams, + 'maxFeePerGas', + 'gasPrice', + ); + ensureFieldIsString(txParams, 'maxFeePerGas'); + break; + case 'maxPriorityFeePerGas': + ensureProperTransactionEnvelopeTypeProvided( + txParams, + 'maxPriorityFeePerGas', + ); + ensureMutuallyExclusiveFieldsNotProvided( + txParams, + 'maxPriorityFeePerGas', + 'gasPrice', + ); + ensureFieldIsString(txParams, 'maxPriorityFeePerGas'); + break; case 'value': - if (typeof value !== 'string') { - throw ethErrors.rpc.invalidParams( - `Invalid transaction params: ${key} is not a string. got: (${value})`, - ); - } + ensureFieldIsString(txParams, 'value'); if (value.toString().includes('-')) { throw ethErrors.rpc.invalidParams( `Invalid transaction value "${value}": not a positive number.`, @@ -90,11 +199,7 @@ export function validateTxParams(txParams) { } break; default: - if (typeof value !== 'string') { - throw ethErrors.rpc.invalidParams( - `Invalid transaction params: ${key} is not a string. got: (${value})`, - ); - } + ensureFieldIsString(txParams, key); } }); } diff --git a/app/scripts/controllers/transactions/lib/util.test.js b/app/scripts/controllers/transactions/lib/util.test.js index f6dd83e4a..ad90faf54 100644 --- a/app/scripts/controllers/transactions/lib/util.test.js +++ b/app/scripts/controllers/transactions/lib/util.test.js @@ -1,4 +1,6 @@ import { strict as assert } from 'assert'; +import { TRANSACTION_ENVELOPE_TYPES } from '../../../../../shared/constants/transaction'; +import { BURN_ADDRESS } from '../../../../../shared/modules/hexstring-utils'; import * as txUtils from './util'; describe('txUtils', function () { @@ -48,6 +50,239 @@ describe('txUtils', function () { message: 'Invalid transaction value "-0x01": not a positive number.', }); }); + + describe('when validating gasPrice', function () { + it('should error when specifying incorrect type', function () { + const txParams = { + gasPrice: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: `Invalid transaction envelope type: specified type "0x2" but included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas`, + }, + ); + }); + + it('should error when gasPrice is not a string', function () { + const txParams = { + gasPrice: 1, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: gasPrice is not a string. got: (1)', + }, + ); + }); + + it('should error when specifying maxFeePerGas', function () { + const txParams = { + gasPrice: '0x1', + maxFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: specified gasPrice but also included maxFeePerGas, these cannot be mixed', + }, + ); + }); + + it('should error when specifying maxPriorityFeePerGas', function () { + const txParams = { + gasPrice: '0x1', + maxPriorityFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: specified gasPrice but also included maxPriorityFeePerGas, these cannot be mixed', + }, + ); + }); + + it('should validate if gasPrice is set with no type or EIP-1559 gas fields', function () { + const txParams = { + gasPrice: '0x1', + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + + it('should validate if gasPrice is set with a type of "0x0"', function () { + const txParams = { + gasPrice: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.LEGACY, + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + }); + + describe('when validating maxFeePerGas', function () { + it('should error when specifying incorrect type', function () { + const txParams = { + maxFeePerGas: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.LEGACY, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction envelope type: specified type "0x0" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2"', + }, + ); + }); + + it('should error when maxFeePerGas is not a string', function () { + const txParams = { + maxFeePerGas: 1, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: maxFeePerGas is not a string. got: (1)', + }, + ); + }); + + it('should error when specifying gasPrice', function () { + const txParams = { + gasPrice: '0x1', + maxFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: specified gasPrice but also included maxFeePerGas, these cannot be mixed', + }, + ); + }); + + it('should validate if maxFeePerGas is set with no type or gasPrice field', function () { + const txParams = { + maxFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + + it('should validate if maxFeePerGas is set with a type of "0x2"', function () { + const txParams = { + maxFeePerGas: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + }); + + describe('when validating maxPriorityFeePerGas', function () { + it('should error when specifying incorrect type', function () { + const txParams = { + maxPriorityFeePerGas: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.LEGACY, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction envelope type: specified type "0x0" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2"', + }, + ); + }); + + it('should error when maxFeePerGas is not a string', function () { + const txParams = { + maxPriorityFeePerGas: 1, + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: maxPriorityFeePerGas is not a string. got: (1)', + }, + ); + }); + + it('should error when specifying gasPrice', function () { + const txParams = { + gasPrice: '0x1', + maxPriorityFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + + assert.throws( + () => { + txUtils.validateTxParams(txParams); + }, + { + message: + 'Invalid transaction params: specified gasPrice but also included maxPriorityFeePerGas, these cannot be mixed', + }, + ); + }); + + it('should validate if maxPriorityFeePerGas is set with no type or gasPrice field', function () { + const txParams = { + maxPriorityFeePerGas: '0x1', + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + + it('should validate if maxPriorityFeePerGas is set with a type of "0x2"', function () { + const txParams = { + maxPriorityFeePerGas: '0x1', + type: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, + to: BURN_ADDRESS, + }; + assert.doesNotThrow(() => txUtils.validateTxParams(txParams)); + }); + }); }); describe('#normalizeTxParams', function () { @@ -58,6 +293,10 @@ describe('txUtils', function () { to: null, data: '68656c6c6f20776f726c64', random: 'hello world', + gasPrice: '1', + maxFeePerGas: '1', + maxPriorityFeePerGas: '1', + type: '1', }; let normalizedTxParams = txUtils.normalizeTxParams(txParams); @@ -89,6 +328,28 @@ describe('txUtils', function () { '0x', 'to should be hex-prefixed', ); + + assert.equal( + normalizedTxParams.gasPrice, + '0x1', + 'gasPrice should be hex-prefixed', + ); + + assert.equal( + normalizedTxParams.maxFeePerGas, + '0x1', + 'maxFeePerGas should be hex-prefixed', + ); + assert.equal( + normalizedTxParams.maxPriorityFeePerGas, + '0x1', + 'maxPriorityFeePerGas should be hex-prefixed', + ); + assert.equal( + normalizedTxParams.type, + '0x1', + 'type should be hex-prefixed', + ); }); }); diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 1b84577b1..4d274a299 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -62,8 +62,11 @@ export default class TxGasUtil { // `eth_estimateGas` can fail if the user has insufficient balance for the // value being sent, or for the gas cost. We don't want to check their // balance here, we just want the gas estimate. The gas price is removed - // to skip those balance checks. We check balance elsewhere. + // to skip those balance checks. We check balance elsewhere. We also delete + // maxFeePerGas and maxPriorityFeePerGas to support EIP-1559 txs. delete txParams.gasPrice; + delete txParams.maxFeePerGas; + delete txParams.maxPriorityFeePerGas; // estimate tx gas requirements return await this.query.estimateGas(txParams); diff --git a/app/scripts/controllers/transactions/tx-gas-utils.test.js b/app/scripts/controllers/transactions/tx-gas-utils.test.js index 635802925..15386e319 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.test.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.test.js @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; -import Transaction from 'ethereumjs-tx'; +import { TransactionFactory } from '@ethereumjs/tx'; +import Common from '@ethereumjs/common'; import { hexToBn, bnToHex } from '../../lib/util'; import TxUtils from './tx-gas-utils'; @@ -31,8 +32,14 @@ describe('txUtils', function () { nonce: '0x3', chainId: 42, }; - const ethTx = new Transaction(txParams); - assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params'); + const ethTx = TransactionFactory.fromTxData(txParams, { + common: new Common({ chain: 'kovan' }), + }); + assert.equal( + ethTx.common.chainIdBN().toNumber(), + 42, + 'chainId is set from tx params', + ); }); }); diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index eed1f6d79..19155d1a2 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -72,12 +72,45 @@ export default class TransactionStateManager extends EventEmitter { * overwriting default keys of the TransactionMeta * @returns {TransactionMeta} the default txMeta object */ - generateTxMeta(opts) { + generateTxMeta(opts = {}) { const netId = this.getNetwork(); const chainId = this.getCurrentChainId(); if (netId === 'loading') { throw new Error('MetaMask is having trouble connecting to the network'); } + + let dappSuggestedGasFees = null; + + // If we are dealing with a transaction suggested by a dapp and not + // an internally created metamask transaction, we need to keep record of + // the originally submitted gasParams. + if ( + opts.txParams && + typeof opts.origin === 'string' && + opts.origin !== 'metamask' + ) { + if (typeof opts.txParams.gasPrice !== 'undefined') { + dappSuggestedGasFees = { + gasPrice: opts.txParams.gasPrice, + }; + } else if ( + typeof opts.txParams.maxFeePerGas !== 'undefined' || + typeof opts.txParams.maxPriorityFeePerGas !== 'undefined' + ) { + dappSuggestedGasFees = { + maxPriorityFeePerGas: opts.txParams.maxPriorityFeePerGas, + maxFeePerGas: opts.txParams.maxFeePerGas, + }; + } + + if (typeof opts.txParams.gas !== 'undefined') { + dappSuggestedGasFees = { + ...dappSuggestedGasFees, + gas: opts.txParams.gas, + }; + } + } + return { id: createId(), time: new Date().getTime(), @@ -85,6 +118,7 @@ export default class TransactionStateManager extends EventEmitter { metamaskNetworkId: netId, chainId, loadingDefaults: true, + dappSuggestedGasFees, ...opts, }; } diff --git a/app/scripts/controllers/transactions/tx-state-manager.test.js b/app/scripts/controllers/transactions/tx-state-manager.test.js index 895fb3626..e8a391faf 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.test.js +++ b/app/scripts/controllers/transactions/tx-state-manager.test.js @@ -10,6 +10,7 @@ import { RINKEBY_CHAIN_ID, KOVAN_NETWORK_ID, } from '../../../../shared/constants/network'; +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import TxStateManager from './tx-state-manager'; import { snapshotFromTxMeta } from './lib/tx-state-history-helpers'; @@ -1074,6 +1075,136 @@ describe('TransactionStateManager', function () { }); }); + describe('#generateTxMeta', function () { + it('generates a txMeta object when supplied no parameters', function () { + // There are currently not safety checks for missing 'opts' but we should + // at least enforce txParams. This is done in the transaction controller + // before *calling* this method, but we should perhaps ensure that + // txParams is provided and validated in this method. + // TODO: this test should fail. + const generatedTransaction = txStateManager.generateTxMeta(); + assert.ok(generatedTransaction); + }); + + it('generates a txMeta object with txParams specified', function () { + const txParams = { + gas: GAS_LIMITS.SIMPLE, + from: '0x0000', + to: '0x000', + value: '0x0', + gasPrice: '0x0', + }; + const generatedTransaction = txStateManager.generateTxMeta({ + txParams, + }); + assert.ok(generatedTransaction); + assert.strictEqual(generatedTransaction.txParams, txParams); + }); + + it('generates a txMeta object with txParams specified using EIP-1559 fields', function () { + const txParams = { + gas: GAS_LIMITS.SIMPLE, + from: '0x0000', + to: '0x000', + value: '0x0', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', + }; + const generatedTransaction = txStateManager.generateTxMeta({ + txParams, + }); + assert.ok(generatedTransaction); + assert.strictEqual(generatedTransaction.txParams, txParams); + }); + + it('records dappSuggestedGasFees when origin is provided and is not "metamask"', function () { + const eip1559GasFeeFields = { + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', + gas: GAS_LIMITS.SIMPLE, + }; + + const legacyGasFeeFields = { + gasPrice: '0x0', + gas: GAS_LIMITS.SIMPLE, + }; + + const eip1559TxParams = { + from: '0x0000', + to: '0x000', + value: '0x0', + ...eip1559GasFeeFields, + }; + + const legacyTxParams = { + from: '0x0000', + to: '0x000', + value: '0x0', + ...legacyGasFeeFields, + }; + const eip1559GeneratedTransaction = txStateManager.generateTxMeta({ + txParams: eip1559TxParams, + origin: 'adappt.com', + }); + const legacyGeneratedTransaction = txStateManager.generateTxMeta({ + txParams: legacyTxParams, + origin: 'adappt.com', + }); + assert.ok( + eip1559GeneratedTransaction, + 'generated EIP1559 transaction should be truthy', + ); + assert.deepStrictEqual( + eip1559GeneratedTransaction.dappSuggestedGasFees, + eip1559GasFeeFields, + 'generated EIP1559 transaction should have appropriate dappSuggestedGasFees', + ); + + assert.ok( + legacyGeneratedTransaction, + 'generated legacy transaction should be truthy', + ); + assert.deepStrictEqual( + legacyGeneratedTransaction.dappSuggestedGasFees, + legacyGasFeeFields, + 'generated legacy transaction should have appropriate dappSuggestedGasFees', + ); + }); + + it('does not record dappSuggestedGasFees when transaction origin is "metamask"', function () { + const txParams = { + gas: GAS_LIMITS.SIMPLE, + from: '0x0000', + to: '0x000', + value: '0x0', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', + }; + const generatedTransaction = txStateManager.generateTxMeta({ + txParams, + origin: 'metamask', + }); + assert.ok(generatedTransaction); + assert.strictEqual(generatedTransaction.dappSuggestedGasFees, null); + }); + + it('does not record dappSuggestedGasFees when transaction origin is not provided', function () { + const txParams = { + gas: GAS_LIMITS.SIMPLE, + from: '0x0000', + to: '0x000', + value: '0x0', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', + }; + const generatedTransaction = txStateManager.generateTxMeta({ + txParams, + }); + assert.ok(generatedTransaction); + assert.strictEqual(generatedTransaction.dappSuggestedGasFees, null); + }); + }); + describe('#clearUnapprovedTxs', function () { it('removes unapproved transactions', function () { const txMetas = [ diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 2cb1b0e81..f9f15e33b 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -1,3 +1,6 @@ +import log from 'loglevel'; + +import { METASWAP_CHAINID_API_HOST_MAP } from '../../../shared/constants/swaps'; import { GOERLI_CHAIN_ID, KOVAN_CHAIN_ID, @@ -5,6 +8,54 @@ import { RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, } from '../../../shared/constants/network'; +import { SECOND } from '../../../shared/constants/time'; +import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; +import { TRANSAK_API_KEY } from '../constants/on-ramp'; + +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); + +/** + * Create a Wyre purchase URL. + * @param {String} address Ethereum destination address + * @returns String + */ +const createWyrePurchaseUrl = async (address) => { + const fiatOnRampUrlApi = `${METASWAP_CHAINID_API_HOST_MAP[MAINNET_CHAIN_ID]}/fiatOnRampUrl?serviceName=wyre&destinationAddress=${address}`; + const wyrePurchaseUrlFallback = `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card`; + try { + const response = await fetchWithTimeout(fiatOnRampUrlApi, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + }); + const parsedResponse = await response.json(); + if (response.ok && parsedResponse.url) { + return parsedResponse.url; + } + log.warn('Failed to create a Wyre purchase URL', parsedResponse); + } catch (err) { + log.warn('Failed to create a Wyre purchase URL', err); + } + return wyrePurchaseUrlFallback; // In case the API call would fail, we return a fallback URL for Wyre's Checkout. +}; + +/** + * Create a Transak Checkout URL. + * API docs here: https://www.notion.so/Query-Parameters-9ec523df3b874ec58cef4fa3a906f238 + * @param {String} address Ethereum destination address + * @returns String + */ +const createTransakUrl = (address) => { + const queryParams = new URLSearchParams({ + apiKey: TRANSAK_API_KEY, + hostURL: 'https://metamask.io', + defaultCryptoCurrency: 'ETH', + walletAddress: address, + }); + return `https://global.transak.com/?${queryParams}`; +}; /** * Gives the caller a url at which the user can acquire eth, depending on the network they are in @@ -16,7 +67,7 @@ import { * chainId does not match any of the specified cases, or if no chainId is given, returns undefined. * */ -export default function getBuyEthUrl({ chainId, address, service }) { +export default async function getBuyEthUrl({ chainId, address, service }) { // default service by network if not specified if (!service) { // eslint-disable-next-line no-param-reassign @@ -25,7 +76,9 @@ export default function getBuyEthUrl({ chainId, address, service }) { switch (service) { case 'wyre': - return `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card`; + return await createWyrePurchaseUrl(address); + case 'transak': + return createTransakUrl(address); case 'metamask-faucet': return 'https://faucet.metamask.io/'; case 'rinkeby-faucet': diff --git a/app/scripts/lib/buy-eth-url.test.js b/app/scripts/lib/buy-eth-url.test.js index 01837c8ef..d15af3e5d 100644 --- a/app/scripts/lib/buy-eth-url.test.js +++ b/app/scripts/lib/buy-eth-url.test.js @@ -1,49 +1,76 @@ import { strict as assert } from 'assert'; +import nock from 'nock'; import { KOVAN_CHAIN_ID, MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, } from '../../../shared/constants/network'; +import { TRANSAK_API_KEY } from '../constants/on-ramp'; import getBuyEthUrl from './buy-eth-url'; +const WYRE_ACCOUNT_ID = 'AC-7AG3W4XH4N2'; +const ETH_ADDRESS = '0x0dcd5d886577d5581b0c524242ef2ee70be3e7bc'; +const MAINNET = { + chainId: MAINNET_CHAIN_ID, + amount: 5, + address: ETH_ADDRESS, +}; +const ROPSTEN = { + chainId: ROPSTEN_CHAIN_ID, +}; +const RINKEBY = { + chainId: RINKEBY_CHAIN_ID, +}; +const KOVAN = { + chainId: KOVAN_CHAIN_ID, +}; + describe('buy-eth-url', function () { - const mainnet = { - chainId: MAINNET_CHAIN_ID, - amount: 5, - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - }; - const ropsten = { - chainId: ROPSTEN_CHAIN_ID, - }; - const rinkeby = { - chainId: RINKEBY_CHAIN_ID, - }; - const kovan = { - chainId: KOVAN_CHAIN_ID, - }; - - it('returns wyre url with address for network 1', function () { - const wyreUrl = getBuyEthUrl(mainnet); + it('returns Wyre url with an ETH address for Ethereum mainnet', async function () { + nock('https://api.metaswap.codefi.network') + .get(`/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`) + .reply(200, { + url: `https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`, + }); + const wyreUrl = await getBuyEthUrl(MAINNET); + assert.equal( + wyreUrl, + `https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`, + ); + nock.cleanAll(); + }); + + it('returns a fallback Wyre url if /orders/reserve API call fails', async function () { + const wyreUrl = await getBuyEthUrl(MAINNET); assert.equal( wyreUrl, - 'https://pay.sendwyre.com/purchase?dest=ethereum:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card', + `https://pay.sendwyre.com/purchase?dest=ethereum:${ETH_ADDRESS}&destCurrency=ETH&accountId=${WYRE_ACCOUNT_ID}&paymentMethod=debit-card`, + ); + }); + + it('returns Transak url with an ETH address for Ethereum mainnet', async function () { + const transakUrl = await getBuyEthUrl({ ...MAINNET, service: 'transak' }); + + assert.equal( + transakUrl, + `https://global.transak.com/?apiKey=${TRANSAK_API_KEY}&hostURL=https%3A%2F%2Fmetamask.io&defaultCryptoCurrency=ETH&walletAddress=${ETH_ADDRESS}`, ); }); - it('returns metamask ropsten faucet for network 3', function () { - const ropstenUrl = getBuyEthUrl(ropsten); + it('returns metamask ropsten faucet for network 3', async function () { + const ropstenUrl = await getBuyEthUrl(ROPSTEN); assert.equal(ropstenUrl, 'https://faucet.metamask.io/'); }); - it('returns rinkeby dapp for network 4', function () { - const rinkebyUrl = getBuyEthUrl(rinkeby); + it('returns rinkeby dapp for network 4', async function () { + const rinkebyUrl = await getBuyEthUrl(RINKEBY); assert.equal(rinkebyUrl, 'https://www.rinkeby.io/'); }); - it('returns kovan github test faucet for network 42', function () { - const kovanUrl = getBuyEthUrl(kovan); + it('returns kovan github test faucet for network 42', async function () { + const kovanUrl = await getBuyEthUrl(KOVAN); assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet'); }); }); diff --git a/app/scripts/lib/ens-ipfs/resolver.js b/app/scripts/lib/ens-ipfs/resolver.js index ff61f00e6..4359a55f8 100644 --- a/app/scripts/lib/ens-ipfs/resolver.js +++ b/app/scripts/lib/ens-ipfs/resolver.js @@ -1,7 +1,7 @@ import namehash from 'eth-ens-namehash'; import Eth from 'ethjs-query'; import EthContract from 'ethjs-contract'; -import contentHash from 'content-hash'; +import contentHash from '@ensdomains/content-hash'; import registryAbi from './contracts/registry'; import resolverAbi from './contracts/resolver'; diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 353bd32f3..c7d1b2c09 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -1,6 +1,8 @@ +import base32Encode from 'base32-encode'; +import base64 from 'base64-js'; import extension from 'extensionizer'; -import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import { SECOND } from '../../../../shared/constants/time'; +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import resolveEnsToIpfsContentId from './resolver'; const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); @@ -81,6 +83,19 @@ export default function setupEnsIpfsResolver({ url = `http://127.0.0.1:43110/${hash}${pathname}${search || ''}${ fragment || '' }`; + } else if (type === 'skynet-ns') { + const padded = hash.padEnd(hash.length + 4 - (hash.length % 4), '='); + const decoded = base64.toByteArray(padded); + + const options = { padding: false }; + const base32EncodedSkylink = base32Encode( + decoded, + 'RFC4648-HEX', + options, + ).toLowerCase(); + url = `https://${base32EncodedSkylink}.siasky.net${pathname}${ + search || '' + }${fragment || ''}`; } } catch (err) { console.warn(err); diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index 33bc80dd9..63f371684 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import assert from 'assert'; +import { strict as assert } from 'assert'; import { ObservableStore } from '@metamask/obs-store'; import { ethErrors } from 'eth-rpc-errors'; import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util'; @@ -177,7 +177,7 @@ export default class TypedMessageManager extends EventEmitter { break; case 'V3': case 'V4': { - assert.strictEqual( + assert.equal( typeof params.data, 'string', '"params.data" must be a string.', @@ -191,18 +191,21 @@ export default class TypedMessageManager extends EventEmitter { data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`, ); - assert.strictEqual( + assert.equal( validation.errors.length, 0, 'Signing data must conform to EIP-712 schema. See https://git.io/fNtcx.', ); - const { chainId } = data.domain; + let { chainId } = data.domain; if (chainId) { const activeChainId = parseInt(this._getCurrentChainId(), 16); assert.ok( !Number.isNaN(activeChainId), `Cannot sign messages for chainId "${chainId}", because MetaMask is switching networks.`, ); + if (typeof chainId === 'string') { + chainId = parseInt(chainId, 16); + } assert.equal( chainId, activeChainId, diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 1b5c8e7e7..9467218ca 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import extension from 'extensionizer'; import { stripHexPrefix } from 'ethereumjs-util'; import BN from 'bn.js'; @@ -74,39 +73,6 @@ const getPlatform = (_) => { return PLATFORM_FIREFOX; }; -/** - * Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee - * - * @param {Object} txParams - Contains data about a transaction - * @param {string} txParams.gas - The gas for a transaction - * @param {string} txParams.gasPrice - The price per gas for the transaction - * @param {string} txParams.value - The value of ETH to send - * @param {string} hexBalance - A balance of ETH represented as a hex string - * @returns {boolean} Whether the balance is greater than or equal to the value plus the value of gas times gasPrice - * - */ -function sufficientBalance(txParams, hexBalance) { - // validate hexBalance is a hex string - assert.equal( - typeof hexBalance, - 'string', - 'sufficientBalance - hexBalance is not a hex string', - ); - assert.equal( - hexBalance.slice(0, 2), - '0x', - 'sufficientBalance - hexBalance is not a hex string', - ); - - const balance = hexToBn(hexBalance); - const value = hexToBn(txParams.value); - const gasLimit = hexToBn(txParams.gas); - const gasPrice = hexToBn(txParams.gasPrice); - - const maxCost = value.add(gasLimit.mul(gasPrice)); - return balance.gte(maxCost); -} - /** * Converts a hex string to a BN object * @@ -196,7 +162,6 @@ function getChainType(chainId) { export { getPlatform, getEnvironmentType, - sufficientBalance, hexToBn, BnMultiplyByFraction, checkForError, diff --git a/app/scripts/lib/util.test.js b/app/scripts/lib/util.test.js index c2753df56..f21c10d5e 100644 --- a/app/scripts/lib/util.test.js +++ b/app/scripts/lib/util.test.js @@ -7,7 +7,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_BACKGROUND, } from '../../../shared/constants/app'; -import { getEnvironmentType, sufficientBalance } from './util'; +import { getEnvironmentType } from './util'; describe('app utils', function () { describe('getEnvironmentType', function () { @@ -68,44 +68,6 @@ describe('app utils', function () { }); }); - describe('SufficientBalance', function () { - it('returns true if max tx cost is equal to balance.', function () { - const tx = { - value: '0x1', - gas: '0x2', - gasPrice: '0x3', - }; - const balance = '0x8'; - - const result = sufficientBalance(tx, balance); - assert.ok(result, 'sufficient balance found.'); - }); - - it('returns true if max tx cost is less than balance.', function () { - const tx = { - value: '0x1', - gas: '0x2', - gasPrice: '0x3', - }; - const balance = '0x9'; - - const result = sufficientBalance(tx, balance); - assert.ok(result, 'sufficient balance found.'); - }); - - it('returns false if max tx cost is more than balance.', function () { - const tx = { - value: '0x1', - gas: '0x2', - gasPrice: '0x3', - }; - const balance = '0x6'; - - const result = sufficientBalance(tx, balance); - assert.ok(!result, 'insufficient balance found.'); - }); - }); - describe('isPrefixedFormattedHexString', function () { it('should return true for valid hex strings', function () { assert.equal( diff --git a/app/scripts/runLockdown.js b/app/scripts/lockdown-run.js similarity index 83% rename from app/scripts/runLockdown.js rename to app/scripts/lockdown-run.js index 2918368e7..f0682654e 100644 --- a/app/scripts/runLockdown.js +++ b/app/scripts/lockdown-run.js @@ -14,7 +14,7 @@ try { // caught and logged here so that the contentscript still gets injected. // This affects Firefox v56 and Waterfox Classic console.error('Lockdown failed:', error); - if (window.sentry && window.sentry.captureException) { - window.sentry.captureException(error); + if (globalThis.sentry && globalThis.sentry.captureException) { + globalThis.sentry.captureException(error); } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 76ac3a74e..1f9aeca20 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -24,6 +24,8 @@ import { CurrencyRateController, PhishingController, NotificationController, + GasFeeController, + TokenListController, } from '@metamask/controllers'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; @@ -31,6 +33,7 @@ import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { MILLISECOND } from '../../shared/constants/time'; +import { hexToDecimal } from '../../ui/helpers/utils/conversions.util'; import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; @@ -73,6 +76,16 @@ export const METAMASK_CONTROLLER_EVENTS = { UPDATE_BADGE: 'updateBadge', }; +/** + * Accounts can be instantiated from simple, HD or the two hardware wallet + * keyring types. Both simple and HD are treated as default but we do special + * case accounts managed by a hardware wallet. + */ +const KEYRING_TYPES = { + LEDGER: 'Ledger Hardware', + TREZOR: 'Trezor Hardware', +}; + export default class MetamaskController extends EventEmitter { /** * @constructor @@ -164,6 +177,38 @@ export default class MetamaskController extends EventEmitter { initState: initState.MetaMetricsController, }); + const gasFeeMessenger = controllerMessenger.getRestricted({ + name: 'GasFeeController', + }); + + this.gasFeeController = new GasFeeController({ + interval: 10000, + messenger: gasFeeMessenger, + getProvider: () => + this.networkController.getProviderAndBlockTracker().provider, + onNetworkStateChange: this.networkController.on.bind( + this.networkController, + NETWORK_EVENTS.NETWORK_DID_CHANGE, + ), + getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( + this.networkController, + ), + getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind( + this, + ), + legacyAPIEndpoint: `https://gas-api.metaswap.codefi.network/networks//gasPrices`, + EIP1559APIEndpoint: `https://gas-api.metaswap.codefi.network/networks//suggestedGasFees`, + getCurrentNetworkLegacyGasAPICompatibility: () => { + const chainId = this.networkController.getCurrentChainId(); + return process.env.IN_TEST || chainId === MAINNET_CHAIN_ID; + }, + getChainId: () => { + return process.env.IN_TEST + ? MAINNET_CHAIN_ID + : this.networkController.getCurrentChainId(); + }, + }); + this.appStateController = new AppStateController({ addUnlockListener: this.on.bind(this, 'unlock'), isUnlocked: this.isUnlocked.bind(this), @@ -182,6 +227,31 @@ export default class MetamaskController extends EventEmitter { state: initState.CurrencyController, }); + const tokenListMessenger = controllerMessenger.getRestricted({ + name: 'TokenListController', + }); + this.tokenListController = new TokenListController({ + chainId: hexToDecimal(this.networkController.getCurrentChainId()), + useStaticTokenList: this.preferencesController.store.getState() + .useStaticTokenList, + onNetworkStateChange: (cb) => + this.networkController.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + provider: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }), + onPreferencesStateChange: this.preferencesController.store.subscribe.bind( + this.preferencesController.store, + ), + messenger: tokenListMessenger, + state: initState.tokenListController, + }); + this.phishingController = new PhishingController(); this.notificationController = new NotificationController( @@ -238,11 +308,13 @@ export default class MetamaskController extends EventEmitter { this.incomingTransactionsController.start(); this.tokenRatesController.start(); this.currencyRateController.start(); + this.tokenListController.start(); } else { this.accountTracker.stop(); this.incomingTransactionsController.stop(); this.tokenRatesController.stop(); this.currencyRateController.stop(); + this.tokenListController.stop(); } }); @@ -326,6 +398,12 @@ export default class MetamaskController extends EventEmitter { getPermittedAccounts: this.permissionsController.getAccounts.bind( this.permissionsController, ), + getProviderConfig: this.networkController.getProviderConfig.bind( + this.networkController, + ), + getEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( + this.networkController, + ), networkStore: this.networkController.networkStore, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, @@ -454,6 +532,8 @@ export default class MetamaskController extends EventEmitter { PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, NotificationController: this.notificationController, + GasFeeController: this.gasFeeController, + TokenListController: this.tokenListController, }); this.memStore = new ComposableObservableStore({ @@ -485,6 +565,8 @@ export default class MetamaskController extends EventEmitter { EnsController: this.ensController.store, ApprovalController: this.approvalController, NotificationController: this.notificationController, + GasFeeController: this.gasFeeController, + TokenListController: this.tokenListController, }, controllerMessenger, }); @@ -678,6 +760,10 @@ export default class MetamaskController extends EventEmitter { setUseBlockie: this.setUseBlockie.bind(this), setUseNonceField: this.setUseNonceField.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this), + setUseStaticTokenList: nodeify( + this.preferencesController.setUseStaticTokenList, + this.preferencesController, + ), setIpfsGateway: this.setIpfsGateway.bind(this), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), @@ -1011,6 +1097,17 @@ export default class MetamaskController extends EventEmitter { this.notificationController.updateViewed, this.notificationController, ), + + // GasFeeController + getGasFeeEstimatesAndStartPolling: nodeify( + this.gasFeeController.getGasFeeEstimatesAndStartPolling, + this.gasFeeController, + ), + + disconnectGasFeeEstimatePoller: nodeify( + this.gasFeeController.disconnectPoller, + this.gasFeeController, + ), }; } @@ -1786,7 +1883,7 @@ export default class MetamaskController extends EventEmitter { const keyring = await this.keyringController.getKeyringForAccount(address); switch (keyring.type) { - case 'Ledger Hardware': { + case KEYRING_TYPES.LEDGER: { return new Promise((_, reject) => { reject( new Error('Ledger does not support eth_getEncryptionPublicKey.'), @@ -1794,7 +1891,7 @@ export default class MetamaskController extends EventEmitter { }); } - case 'Trezor Hardware': { + case KEYRING_TYPES.TREZOR: { return new Promise((_, reject) => { reject( new Error('Trezor does not support eth_getEncryptionPublicKey.'), @@ -1933,32 +2030,63 @@ export default class MetamaskController extends EventEmitter { cb(null, this.getState()); } + /** + * Method to return a boolean if the keyring for the currently selected + * account is a ledger or trezor keyring. + * TODO: remove this method when Ledger and Trezor release their supported + * client utilities for EIP-1559 + * @returns {boolean} true if the keyring type supports EIP-1559 + */ + getCurrentAccountEIP1559Compatibility() { + const selectedAddress = this.preferencesController.getSelectedAddress(); + const keyring = this.keyringController.getKeyringForAccount( + selectedAddress, + ); + return ( + keyring.type !== KEYRING_TYPES.LEDGER && + keyring.type !== KEYRING_TYPES.TREZOR + ); + } + //============================================================================= // END (VAULT / KEYRING RELATED METHODS) //============================================================================= /** - * Allows a user to attempt to cancel a previously submitted transaction by creating a new - * transaction. - * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel - * @param {string} [customGasPrice] - the hex value to use for the cancel transaction + * Allows a user to attempt to cancel a previously submitted transaction + * by creating a new transaction. + * @param {number} originalTxId - the id of the txMeta that you want to + * attempt to cancel + * @param {import( + * './controllers/transactions' + * ).CustomGasSettings} [customGasSettings] - overrides to use for gas params + * instead of allowing this method to generate them * @returns {Object} MetaMask state */ - async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) { + async createCancelTransaction(originalTxId, customGasSettings) { await this.txController.createCancelTransaction( originalTxId, - customGasPrice, - customGasLimit, + customGasSettings, ); const state = await this.getState(); return state; } - async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) { + /** + * Allows a user to attempt to speed up a previously submitted transaction + * by creating a new transaction. + * @param {number} originalTxId - the id of the txMeta that you want to + * attempt to speed up + * @param {import( + * './controllers/transactions' + * ).CustomGasSettings} [customGasSettings] - overrides to use for gas params + * instead of allowing this method to generate them + * @returns {Object} MetaMask state + */ + async createSpeedUpTransaction(originalTxId, customGasSettings) { await this.txController.createSpeedUpTransaction( originalTxId, - customGasPrice, - customGasLimit, + customGasSettings, ); const state = await this.getState(); return state; diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 87fed2aef..f75bf7b50 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -21,6 +21,11 @@ const firstTimeState = { rpcUrl: 'http://localhost:8545', chainId: '0x539', }, + networkDetails: { + EIPS: { + 1559: false, + }, + }, }, }; diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 389b4ef4a..693537f80 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -5,68 +5,130 @@ // The `migrate` function receives the previous // config data format, and returns the new one. +import m002 from './002'; +import m003 from './003'; +import m004 from './004'; +import m005 from './005'; +import m006 from './006'; +import m007 from './007'; +import m008 from './008'; +import m009 from './009'; +import m010 from './010'; +import m011 from './011'; +import m012 from './012'; +import m013 from './013'; +import m014 from './014'; +import m015 from './015'; +import m016 from './016'; +import m017 from './017'; +import m018 from './018'; +import m019 from './019'; +import m020 from './020'; +import m021 from './021'; +import m022 from './022'; +import m023 from './023'; +import m024 from './024'; +import m025 from './025'; +import m026 from './026'; +import m027 from './027'; +import m028 from './028'; +import m029 from './029'; +import m030 from './030'; +import m031 from './031'; +import m032 from './032'; +import m033 from './033'; +import m034 from './034'; +import m035 from './035'; +import m036 from './036'; +import m037 from './037'; +import m038 from './038'; +import m039 from './039'; +import m040 from './040'; +import m041 from './041'; +import m042 from './042'; +import m043 from './043'; +import m044 from './044'; +import m045 from './045'; +import m046 from './046'; +import m047 from './047'; +import m048 from './048'; +import m049 from './049'; +import m050 from './050'; +import m051 from './051'; +import m052 from './052'; +import m053 from './053'; +import m054 from './054'; +import m055 from './055'; +import m056 from './056'; +import m057 from './057'; +import m058 from './058'; +import m059 from './059'; +import m060 from './060'; +import m061 from './061'; +import m062 from './062'; + const migrations = [ - require('./002').default, - require('./003').default, - require('./004').default, - require('./005').default, - require('./006').default, - require('./007').default, - require('./008').default, - require('./009').default, - require('./010').default, - require('./011').default, - require('./012').default, - require('./013').default, - require('./014').default, - require('./015').default, - require('./016').default, - require('./017').default, - require('./018').default, - require('./019').default, - require('./020').default, - require('./021').default, - require('./022').default, - require('./023').default, - require('./024').default, - require('./025').default, - require('./026').default, - require('./027').default, - require('./028').default, - require('./029').default, - require('./030').default, - require('./031').default, - require('./032').default, - require('./033').default, - require('./034').default, - require('./035').default, - require('./036').default, - require('./037').default, - require('./038').default, - require('./039').default, - require('./040').default, - require('./041').default, - require('./042').default, - require('./043').default, - require('./044').default, - require('./045').default, - require('./046').default, - require('./047').default, - require('./048').default, - require('./049').default, - require('./050').default, - require('./051').default, - require('./052').default, - require('./053').default, - require('./054').default, - require('./055').default, - require('./056').default, - require('./057').default, - require('./058').default, - require('./059').default, - require('./060').default, - require('./061').default, - require('./062').default, + m002, + m003, + m004, + m005, + m006, + m007, + m008, + m009, + m010, + m011, + m012, + m013, + m014, + m015, + m016, + m017, + m018, + m019, + m020, + m021, + m022, + m023, + m024, + m025, + m026, + m027, + m028, + m029, + m030, + m031, + m032, + m033, + m034, + m035, + m036, + m037, + m038, + m039, + m040, + m041, + m042, + m043, + m044, + m045, + m046, + m047, + m048, + m049, + m050, + m051, + m052, + m053, + m054, + m055, + m056, + m057, + m058, + m059, + m060, + m061, + m062, ]; export default migrations; diff --git a/app/scripts/initSentry.js b/app/scripts/sentry-install.js similarity index 100% rename from app/scripts/initSentry.js rename to app/scripts/sentry-install.js diff --git a/app/scripts/ui.js b/app/scripts/ui.js index b00c3d6da..ee4539370 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -2,6 +2,9 @@ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; import '@formatjs/intl-relativetimeformat/polyfill'; +// dev only, "react-devtools" import is skipped in prod builds +import 'react-devtools'; + import PortStream from 'extension-port-stream'; import extension from 'extensionizer'; diff --git a/development/build/scripts.js b/development/build/scripts.js index f0e399d47..b334f018a 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -1,19 +1,29 @@ +const { callbackify } = require('util'); +const path = require('path'); +const { writeFileSync, readFileSync } = require('fs'); const EventEmitter = require('events'); const gulp = require('gulp'); const watch = require('gulp-watch'); const source = require('vinyl-source-stream'); const buffer = require('vinyl-buffer'); const log = require('fancy-log'); -const watchify = require('watchify'); const browserify = require('browserify'); -const envify = require('loose-envify/custom'); -const sourcemaps = require('gulp-sourcemaps'); -const terser = require('gulp-terser-js'); +const watchify = require('watchify'); const babelify = require('babelify'); const brfs = require('brfs'); +const envify = require('loose-envify/custom'); +const sourcemaps = require('gulp-sourcemaps'); +const applySourceMap = require('vinyl-sourcemaps-apply'); const pify = require('pify'); +const through = require('through2'); const endOfStream = pify(require('end-of-stream')); const labeledStreamSplicer = require('labeled-stream-splicer').obj; +const wrapInStream = require('pumpify').obj; +const Sqrl = require('squirrelly'); +const lavaPack = require('@lavamoat/lavapack'); +const terser = require('terser'); + +const bifyModuleGroups = require('bify-module-groups'); const metamaskrc = require('rc')('metamask', { INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID, @@ -25,9 +35,8 @@ const metamaskrc = require('rc')('metamask', { 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496', }); -const { version } = require('../../package.json'); - -const packageJSON = require('../../package.json'); +const { streamFlatMap } = require('../stream-flat-map.js'); +const baseManifest = require('../../app/manifest/_base.json'); const { createTask, composeParallel, @@ -37,17 +46,6 @@ const { module.exports = createScriptTasks; -const dependencies = Object.keys( - (packageJSON && packageJSON.dependencies) || {}, -); -const materialUIDependencies = ['@material-ui/core']; -const reactDepenendencies = dependencies.filter((dep) => dep.match(/react/u)); - -const externalDependenciesMap = { - background: ['3box'], - ui: [...materialUIDependencies, ...reactDepenendencies], -}; - function createScriptTasks({ browserPlatforms, livereload }) { // internal tasks const core = { @@ -69,72 +67,43 @@ function createScriptTasks({ browserPlatforms, livereload }) { // production prod: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:prod' }), }; - const deps = { - background: createTasksForBuildJsDeps({ - label: 'bg-libs', - key: 'background', - }), - ui: createTasksForBuildJsDeps({ label: 'ui-libs', key: 'ui' }), - }; // high level tasks - const prod = composeParallel(deps.background, deps.ui, core.prod); - - const { dev, testDev } = core; - - const test = composeParallel(deps.background, deps.ui, core.test); + const { dev, test, testDev, prod } = core; + return { dev, test, testDev, prod }; - return { prod, dev, testDev, test }; - - function createTasksForBuildJsDeps({ key, label }) { - return createTask( - `scripts:deps:${key}`, - createNormalBundle({ - label, - destFilepath: `${label}.js`, - modulesToExpose: externalDependenciesMap[key], - devMode: false, + function createTasksForBuildJsExtension({ taskPrefix, devMode, testing }) { + const standardEntryPoints = ['background', 'ui', 'phishing-detect']; + const standardSubtask = createTask( + `${taskPrefix}:standardEntryPoints`, + createFactoredBuild({ + entryFiles: standardEntryPoints.map( + (label) => `./app/scripts/${label}.js`, + ), + devMode, + testing, browserPlatforms, }), ); - } - - function createTasksForBuildJsExtension({ taskPrefix, devMode, testing }) { - const standardBundles = [ - 'background', - 'ui', - 'phishing-detect', - 'initSentry', - ]; - - const standardSubtasks = standardBundles.map((label) => { - let extraEntries; - if (devMode && label === 'ui') { - extraEntries = ['./development/require-react-devtools.js']; - } - return createTask( - `${taskPrefix}:${label}`, - createBundleTaskForBuildJsExtensionNormal({ - label, - devMode, - testing, - extraEntries, - }), - ); - }); // inpage must be built before contentscript // because inpage bundle result is included inside contentscript const contentscriptSubtask = createTask( `${taskPrefix}:contentscript`, - createTaskForBuildJsExtensionContentscript({ devMode, testing }), + createTaskForBundleContentscript({ devMode, testing }), ); // this can run whenever const disableConsoleSubtask = createTask( `${taskPrefix}:disable-console`, - createTaskForBuildJsExtensionDisableConsole({ devMode }), + createTaskForBundleDisableConsole({ devMode }), + ); + + // this can run whenever + const installSentrySubtask = createTask( + `${taskPrefix}:sentry`, + createTaskForBundleSentry({ devMode }), ); // task for initiating browser livereload @@ -155,37 +124,28 @@ function createScriptTasks({ browserPlatforms, livereload }) { // make each bundle run in a separate process const allSubtasks = [ - ...standardSubtasks, + standardSubtask, contentscriptSubtask, disableConsoleSubtask, + installSentrySubtask, ].map((subtask) => runInChildProcess(subtask)); - // const allSubtasks = [...standardSubtasks, contentscriptSubtask].map(subtask => (subtask)) // make a parent task that runs each task in a child thread return composeParallel(initiateLiveReload, ...allSubtasks); } - function createBundleTaskForBuildJsExtensionNormal({ - label, - devMode, - testing, - extraEntries, - }) { + function createTaskForBundleDisableConsole({ devMode }) { + const label = 'disable-console'; return createNormalBundle({ label, entryFilepath: `./app/scripts/${label}.js`, destFilepath: `${label}.js`, - extraEntries, - externalDependencies: devMode - ? undefined - : externalDependenciesMap[label], devMode, - testing, browserPlatforms, }); } - function createTaskForBuildJsExtensionDisableConsole({ devMode }) { - const label = 'disable-console'; + function createTaskForBundleSentry({ devMode }) { + const label = 'sentry-install'; return createNormalBundle({ label, entryFilepath: `./app/scripts/${label}.js`, @@ -195,7 +155,8 @@ function createScriptTasks({ browserPlatforms, livereload }) { }); } - function createTaskForBuildJsExtensionContentscript({ devMode, testing }) { + // the "contentscript" bundle contains the "inpage" bundle + function createTaskForBundleContentscript({ devMode, testing }) { const inpage = 'inpage'; const contentscript = 'contentscript'; return composeSeries( @@ -203,9 +164,6 @@ function createScriptTasks({ browserPlatforms, livereload }) { label: inpage, entryFilepath: `./app/scripts/${inpage}.js`, destFilepath: `${inpage}.js`, - externalDependencies: devMode - ? undefined - : externalDependenciesMap[inpage], devMode, testing, browserPlatforms, @@ -214,9 +172,6 @@ function createScriptTasks({ browserPlatforms, livereload }) { label: contentscript, entryFilepath: `./app/scripts/${contentscript}.js`, destFilepath: `${contentscript}.js`, - externalDependencies: devMode - ? undefined - : externalDependenciesMap[contentscript], devMode, testing, browserPlatforms, @@ -225,12 +180,120 @@ function createScriptTasks({ browserPlatforms, livereload }) { } } +function createFactoredBuild({ + entryFiles, + devMode, + testing, + browserPlatforms, +}) { + return async function () { + // create bundler setup and apply defaults + const buildConfiguration = createBuildConfiguration(); + buildConfiguration.label = 'primary'; + const { bundlerOpts, events } = buildConfiguration; + + // devMode options + const reloadOnChange = Boolean(devMode); + const minify = Boolean(devMode) === false; + + const envVars = getEnvironmentVariables({ devMode, testing }); + setupBundlerDefaults(buildConfiguration, { + devMode, + envVars, + reloadOnChange, + minify, + }); + + // set bundle entries + bundlerOpts.entries = [...entryFiles]; + + // setup bundle factoring with bify-module-groups plugin + Object.assign(bundlerOpts, bifyModuleGroups.plugin.args); + bundlerOpts.plugin = [...bundlerOpts.plugin, [bifyModuleGroups.plugin]]; + + // instrument pipeline + let sizeGroupMap; + events.on('configurePipeline', ({ pipeline }) => { + // to be populated by the group-by-size transform + sizeGroupMap = new Map(); + pipeline.get('groups').unshift( + // factor modules + bifyModuleGroups.groupByFactor({ + entryFileToLabel(filepath) { + return path.parse(filepath).name; + }, + }), + // cap files at 2 mb + bifyModuleGroups.groupBySize({ + sizeLimit: 2e6, + groupingMap: sizeGroupMap, + }), + ); + pipeline.get('vinyl').unshift( + // convert each module group into a stream with a single vinyl file + streamFlatMap((moduleGroup) => { + const filename = `${moduleGroup.label}.js`; + const childStream = wrapInStream( + moduleGroup.stream, + lavaPack({ raw: true, hasExports: true, includePrelude: false }), + source(filename), + ); + return childStream; + }), + buffer(), + ); + // setup bundle destination + browserPlatforms.forEach((platform) => { + const dest = `./dist/${platform}/`; + pipeline.get('dest').push(gulp.dest(dest)); + }); + }); + + // wait for bundle completion for postprocessing + events.on('bundleDone', () => { + const commonSet = sizeGroupMap.get('common'); + // create entry points for each file + for (const [groupLabel, groupSet] of sizeGroupMap.entries()) { + // skip "common" group, they are added tp all other groups + if (groupSet === commonSet) continue; + + switch (groupLabel) { + case 'ui': { + renderHtmlFile('popup', groupSet, commonSet, browserPlatforms); + renderHtmlFile( + 'notification', + groupSet, + commonSet, + browserPlatforms, + ); + renderHtmlFile('home', groupSet, commonSet, browserPlatforms); + break; + } + case 'phishing-detect': { + renderHtmlFile('phishing', groupSet, commonSet, browserPlatforms); + break; + } + case 'background': { + renderHtmlFile('background', groupSet, commonSet, browserPlatforms); + break; + } + default: { + throw new Error(`buildsys - unknown groupLabel "${groupLabel}"`); + } + } + } + }); + + await bundleIt(buildConfiguration); + }; +} + function createNormalBundle({ + label, destFilepath, entryFilepath, extraEntries = [], modulesToExpose, - externalDependencies, devMode, testing, browserPlatforms, @@ -238,12 +301,19 @@ function createNormalBundle({ return async function () { // create bundler setup and apply defaults const buildConfiguration = createBuildConfiguration(); + buildConfiguration.label = label; const { bundlerOpts, events } = buildConfiguration; + // devMode options + const reloadOnChange = Boolean(devMode); + const minify = Boolean(devMode) === false; + const envVars = getEnvironmentVariables({ devMode, testing }); setupBundlerDefaults(buildConfiguration, { devMode, envVars, + reloadOnChange, + minify, }); // set bundle entries @@ -256,14 +326,6 @@ function createNormalBundle({ bundlerOpts.require = bundlerOpts.require.concat(modulesToExpose); } - if (externalDependencies) { - // there doesnt seem to be a standard bify option for this - // so we'll put it here but manually call it after bundle - bundlerOpts.manualExternal = bundlerOpts.manualExternal.concat( - externalDependencies, - ); - } - // instrument pipeline events.on('configurePipeline', ({ pipeline }) => { // convert bundle stream to gulp vinyl stream @@ -282,23 +344,25 @@ function createNormalBundle({ } function createBuildConfiguration() { + const label = '(unnamed bundle)'; const events = new EventEmitter(); const bundlerOpts = { entries: [], transform: [], plugin: [], require: [], - // not a standard bify option + // non-standard bify options manualExternal: [], + manualIgnore: [], }; - return { bundlerOpts, events }; + return { label, bundlerOpts, events }; } -function setupBundlerDefaults(buildConfiguration, { devMode, envVars }) { +function setupBundlerDefaults( + buildConfiguration, + { devMode, envVars, reloadOnChange, minify }, +) { const { bundlerOpts } = buildConfiguration; - // devMode options - const reloadOnChange = Boolean(devMode); - const minify = Boolean(devMode) === false; Object.assign(bundlerOpts, { // source transforms @@ -314,6 +378,11 @@ function setupBundlerDefaults(buildConfiguration, { devMode, envVars }) { debug: true, }); + // ensure react-devtools are not included in non-dev builds + if (!devMode) { + bundlerOpts.manualIgnore.push('react-devtools'); + } + // inject environment variables via node-style `process.env` if (envVars) { bundlerOpts.transform.push([envify(envVars), { global: true }]); @@ -351,17 +420,33 @@ function setupReloadOnChange({ bundlerOpts, events }) { } function setupMinification(buildConfiguration) { + const minifyOpts = { + mangle: { + reserved: ['MetamaskInpageProvider'], + }, + }; const { events } = buildConfiguration; events.on('configurePipeline', ({ pipeline }) => { pipeline.get('minify').push( - terser({ - mangle: { - reserved: ['MetamaskInpageProvider'], - }, - sourceMap: { - content: true, - }, - }), + // this is the "gulp-terser-js" wrapper around the latest version of terser + through.obj( + callbackify(async (file, _enc) => { + const input = { + [file.sourceMap.file]: file.contents.toString(), + }; + const opts = { + sourceMap: { + filename: file.sourceMap.file, + content: file.sourceMap, + }, + ...minifyOpts, + }; + const res = await terser.minify(input, opts); + file.contents = Buffer.from(res.code); + applySourceMap(file, res.map); + return file; + }), + ), ); }); } @@ -383,20 +468,26 @@ function setupSourcemaps(buildConfiguration, { devMode }) { } async function bundleIt(buildConfiguration) { - const { bundlerOpts, events } = buildConfiguration; + const { label, bundlerOpts, events } = buildConfiguration; const bundler = browserify(bundlerOpts); - // manually apply non-standard option + // manually apply non-standard options bundler.external(bundlerOpts.manualExternal); + bundler.ignore(bundlerOpts.manualIgnore); // output build logs to terminal bundler.on('log', log); // forward update event (used by watchify) bundler.on('update', () => performBundle()); + + console.log(`bundle start: "${label}"`); await performBundle(); + console.log(`bundle end: "${label}"`); async function performBundle() { // this pipeline is created for every bundle // the labels are all the steps you can hook into const pipeline = labeledStreamSplicer([ + 'groups', + [], 'vinyl', [], 'sourcemaps:init', @@ -415,7 +506,11 @@ async function bundleIt(buildConfiguration) { bundleStream.pipe(pipeline); // nothing will consume pipeline, so let it flow pipeline.resume(); + await endOfStream(pipeline); + + // call the completion event to handle any post-processing + events.emit('bundleDone'); } } @@ -427,12 +522,15 @@ function getEnvironmentVariables({ devMode, testing }) { return { METAMASK_DEBUG: devMode, METAMASK_ENVIRONMENT: environment, - METAMASK_VERSION: version, + METAMASK_VERSION: baseManifest.version, NODE_ENV: devMode ? 'development' : 'production', IN_TEST: testing ? 'true' : false, PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', CONF: devMode ? metamaskrc : {}, + SHOW_EIP_1559_UI: + process.env.SHOW_EIP_1559_UI === '1' || + metamaskrc.SHOW_EIP_1559_UI === '1', SENTRY_DSN: process.env.SENTRY_DSN, SENTRY_DSN_DEV: metamaskrc.SENTRY_DSN_DEV, INFURA_PROJECT_ID: testing @@ -475,6 +573,20 @@ function getEnvironment({ devMode, testing }) { return 'other'; } +function renderHtmlFile(htmlName, groupSet, commonSet, browserPlatforms) { + const htmlFilePath = `./app/${htmlName}.html`; + const htmlTemplate = readFileSync(htmlFilePath, 'utf8'); + const jsBundles = [...commonSet.values(), ...groupSet.values()].map( + (label) => `./${label}.js`, + ); + const htmlOutput = Sqrl.render(htmlTemplate, { jsBundles }); + browserPlatforms.forEach((platform) => { + const dest = `./dist/${platform}/${htmlName}.html`; + // we dont have a way of creating async events atm + writeFileSync(dest, htmlOutput); + }); +} + function beep() { process.stdout.write('\x07'); } diff --git a/development/build/static.js b/development/build/static.js index 9ae309e53..300134806 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -40,9 +40,8 @@ const copyTargets = [ dest: ``, }, { - src: `./app/`, - pattern: `*.html`, - dest: ``, + src: `./app/loading.html`, + dest: `loading.html`, }, { src: `./node_modules/globalthis/dist/browser.js`, @@ -50,12 +49,16 @@ const copyTargets = [ }, { src: `./node_modules/ses/dist/lockdown.cjs`, - dest: `lockdown.js`, + dest: `lockdown-install.js`, }, { - src: `./app/scripts/`, - pattern: `runLockdown.js`, - dest: ``, + src: `./app/scripts/lockdown-run.js`, + dest: `lockdown-run.js`, + }, + { + // eslint-disable-next-line node/no-extraneous-require + src: require.resolve('@lavamoat/lavapack/src/runtime-cjs.js'), + dest: `runtime-cjs.js`, }, ]; @@ -76,7 +79,7 @@ for (const tag of languageTags) { const copyTargetsDev = [ ...copyTargets, { - src: './app/scripts/', + src: './development', pattern: '/chromereload.js', dest: ``, }, diff --git a/development/build/task.js b/development/build/task.js index bc6f92005..22c70ad4a 100644 --- a/development/build/task.js +++ b/development/build/task.js @@ -68,9 +68,21 @@ function runInChildProcess(task) { ); } return instrumentForTaskStats(taskName, async () => { - const childProcess = spawn('yarn', ['build', taskName, '--skip-stats'], { - env: process.env, - }); + let childProcess; + // don't run subprocesses in lavamoat for dev mode if main process not run in lavamoat + if ( + process.env.npm_lifecycle_event === 'build:dev' || + (taskName.includes('scripts:core:dev') && + !process.argv[0].includes('lavamoat')) + ) { + childProcess = spawn('yarn', ['build:dev', taskName, '--skip-stats'], { + env: process.env, + }); + } else { + childProcess = spawn('yarn', ['build', taskName, '--skip-stats'], { + env: process.env, + }); + } // forward logs to main process // skip the first stdout event (announcing the process command) childProcess.stdout.once('data', () => { diff --git a/development/lib/exit-with-error.js b/development/lib/exit-with-error.js new file mode 100644 index 000000000..1ef855887 --- /dev/null +++ b/development/lib/exit-with-error.js @@ -0,0 +1,16 @@ +/** + * Exit the process with an error message. + * + * Note that this should be called before the process ends, but it will not + * itself end the process. This is because the Node.js documentation strongly + * advises against calling `process.exit` directly. + * + * @param {string} errorMessage - The error message that is causing the non- + * zero exit code. + */ +function exitWithError(errorMessage) { + console.error(errorMessage); + process.exitCode = 1; +} + +module.exports = { exitWithError }; diff --git a/development/lib/retry.js b/development/lib/retry.js new file mode 100644 index 000000000..356f1a08f --- /dev/null +++ b/development/lib/retry.js @@ -0,0 +1,23 @@ +/** + * Run the given function, retrying it upon failure until reaching the + * specified number of retries. + * + * @param {number} retries - The number of retries upon failure to attempt. + * @param {function} functionToRetry - The function that will be retried upon failure. + */ +async function retry(retries, functionToRetry) { + let attempts = 0; + while (attempts <= retries) { + try { + await functionToRetry(); + return; + } catch (error) { + console.error(error); + } finally { + attempts += 1; + } + } + throw new Error('Retry limit reached'); +} + +module.exports = { retry }; diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index a1f6e3659..8403a4843 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -2,6 +2,7 @@ const { promises: fs } = require('fs'); const path = require('path'); const fetch = require('node-fetch'); +const glob = require('fast-glob'); const VERSION = require('../dist/chrome/manifest.json').version; // eslint-disable-line import/no-unresolved start().catch(console.error); @@ -39,21 +40,35 @@ async function start() { .join(', '); // links to bundle browser builds - const bundles = [ - 'background', - 'ui', - 'inpage', - 'contentscript', - 'ui-libs', - 'bg-libs', - 'phishing-detect', - ]; - const bundleLinks = bundles - .map((bundle) => { - const url = `${BUILD_LINK_BASE}/build-artifacts/source-map-explorer/${bundle}.html`; - return `${bundle}`; - }) - .join(', '); + const bundles = {}; + const fileType = '.html'; + const sourceMapRoot = '/build-artifacts/source-map-explorer/'; + const bundleFiles = await glob(`.${sourceMapRoot}*${fileType}`); + + bundleFiles.forEach((bundleFile) => { + const fileName = bundleFile.split(sourceMapRoot)[1]; + const bundleName = fileName.split(fileType)[0]; + const url = `${BUILD_LINK_BASE}${sourceMapRoot}${fileName}`; + let fileRoot = bundleName; + let fileIndex = bundleName.match(/-[0-9]{1,}$/u)?.index; + + if (fileIndex) { + fileRoot = bundleName.slice(0, fileIndex); + fileIndex = bundleName.slice(fileIndex + 1, bundleName.length); + } + + const link = `${fileIndex || fileRoot}`; + + if (fileRoot in bundles) { + bundles[fileRoot].push(link); + } else { + bundles[fileRoot] = [link]; + } + }); + + const bundleMarkup = `
    ${Object.keys(bundles) + .map((key) => `
  • ${key}: ${bundles[key].join(', ')}
  • `) + .join('')}
`; const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`; const coverageLink = `Report`; @@ -70,11 +85,14 @@ async function start() { const contentRows = [ `builds: ${buildLinks}`, - `bundle viz: ${bundleLinks}`, `build viz: ${depVizLink}`, `code coverage: ${coverageLink}`, `storybook: ${storybookLink}`, `all artifacts`, + `
+ bundle viz: + ${bundleMarkup} +
`, ]; const hiddenContent = `
    ${contentRows .map((row) => `
  • ${row}
  • `) diff --git a/development/require-react-devtools.js b/development/require-react-devtools.js deleted file mode 100644 index 9af12aa87..000000000 --- a/development/require-react-devtools.js +++ /dev/null @@ -1 +0,0 @@ -require('react-devtools'); diff --git a/development/sourcemap-validator.js b/development/sourcemap-validator.js index 15feb56f1..504e87e80 100644 --- a/development/sourcemap-validator.js +++ b/development/sourcemap-validator.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const { SourceMapConsumer } = require('source-map'); const pify = require('pify'); +const { codeFrameColumns } = require('@babel/code-frame'); const fsAsync = pify(fs); @@ -20,13 +21,12 @@ start().catch((error) => { async function start() { const targetFiles = [ - `background.js`, - // `bg-libs`, skipped because source maps are invalid due to browserify bug: https://github.com/browserify/browserify/issues/1971 + `common-0.js`, + `background-0.js`, + `ui-0.js`, + 'phishing-detect-0.js', // `contentscript.js`, skipped because the validator is erroneously sampling the inlined `inpage.js` script `inpage.js`, - 'phishing-detect.js', - `ui.js`, - // `ui-libs.js`, skipped because source maps are invalid due to browserify bug: https://github.com/browserify/browserify/issues/1971 ]; let valid = true; @@ -102,7 +102,6 @@ async function validateSourcemapForFile({ buildName }) { const buildLines = rawBuild.split('\n'); const targetString = 'new Error'; - // const targetString = 'null' const matchesPerLine = buildLines.map((line) => indicesOf(targetString, line), ); @@ -114,26 +113,35 @@ async function validateSourcemapForFile({ buildName }) { // warn if source content is missing if (!result.source) { valid = false; - console.warn( - `!! missing source for position: ${JSON.stringify(position)}`, + const location = { + start: { line: position.line, column: position.column + 1 }, + }; + const codeSample = codeFrameColumns(rawBuild, location, { + message: `missing source for position`, + highlightCode: true, + }); + console.error( + `missing source for position, in bundle "${buildName}"\n${codeSample}`, ); - // const buildLine = buildLines[position.line - 1] - console.warn(` origin in build:`); - console.warn(` ${buildLines[position.line - 2]}`); - console.warn(`-> ${buildLines[position.line - 1]}`); - console.warn(` ${buildLines[position.line - 0]}`); return; } const sourceContent = consumer.sourceContentFor(result.source); const sourceLines = sourceContent.split('\n'); - const line = sourceLines[result.line - 1]; + const sourceLine = sourceLines[result.line - 1]; // this sometimes includes the whole line though we tried to match somewhere in the middle - const portion = line.slice(result.column); - const isMaybeValid = portion.includes(targetString); - if (!isMaybeValid) { + const portion = sourceLine.slice(result.column); + const foundValidSource = portion.includes(targetString); + if (!foundValidSource) { valid = false; + const location = { + start: { line: result.line + 1, column: result.column + 1 }, + }; + const codeSample = codeFrameColumns(sourceContent, location, { + message: `expected to see ${JSON.stringify(targetString)}`, + highlightCode: true, + }); console.error( - `Sourcemap seems invalid:\n${getFencedCode(result.source, line)}`, + `Sourcemap seems invalid, ${result.source}\n${codeSample}`, ); } }); @@ -142,27 +150,6 @@ async function validateSourcemapForFile({ buildName }) { return valid; } -const CODE_FENCE_LENGTH = 80; -const TITLE_PADDING_LENGTH = 1; - -function getFencedCode(filename, code) { - const title = `${' '.repeat(TITLE_PADDING_LENGTH)}${filename}${' '.repeat( - TITLE_PADDING_LENGTH, - )}`; - const openingFenceLength = Math.max( - CODE_FENCE_LENGTH - (filename.length + TITLE_PADDING_LENGTH * 2), - 0, - ); - const startOpeningFenceLength = Math.floor(openingFenceLength / 2); - const endOpeningFenceLength = Math.ceil(openingFenceLength / 2); - const openingFence = `${'='.repeat( - startOpeningFenceLength, - )}${title}${'='.repeat(endOpeningFenceLength)}`; - const closingFence = '='.repeat(CODE_FENCE_LENGTH); - - return `${openingFence}\n${code}\n${closingFence}\n`; -} - function indicesOf(substring, string) { const a = []; let i = -1; diff --git a/development/stream-flat-map.js b/development/stream-flat-map.js new file mode 100644 index 000000000..0e84f3514 --- /dev/null +++ b/development/stream-flat-map.js @@ -0,0 +1,43 @@ +const { PassThrough: ThroughStream } = require('stream'); +// eslint-ignore-next-line node/no-extraneous-require +const duplexify = require('duplexify').obj; + +module.exports = { + streamFlatMap, + asyncGeneratorToStream, +}; + +// returns an async generator that maps each chunk to a stream with the specified +// "entryToStream" mapping fn, and forwards child streams out +// useable with streams.pipeline +function streamFlatMap(entryToStream) { + const duplex = asyncGeneratorToStream(flatMapGenerator); + return duplex; + + async function* flatMapGenerator(source) { + for await (const entry of source) { + const subStream = entryToStream(entry); + yield* subStream; + } + } +} + +// this stupid utility turns an async iterator factory into a duplex stream +function asyncGeneratorToStream(factoryFn) { + const writableStream = new ThroughStream({ objectMode: true }); + const readableStream = new ThroughStream({ objectMode: true }); + const duplex = duplexify(writableStream, readableStream); + const asyncIter = factoryFn(writableStream); + // drain iterator into readable stream + process.nextTick(async () => { + try { + for await (const item of asyncIter) { + readableStream.write(item); + } + readableStream.end(); + } catch (err) { + readableStream.destroy(err); + } + }); + return duplex; +} diff --git a/jest.config.js b/jest.config.js index 09bc15c14..425f7580f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,10 +5,10 @@ module.exports = { coveragePathIgnorePatterns: ['.stories.js', '.snap'], coverageThreshold: { global: { - branches: 32.75, - functions: 40, - lines: 42.29, - statements: 42.83, + branches: 45.24, + functions: 51.94, + lines: 58.36, + statements: 58.6, }, }, setupFiles: ['./test/setup.js', './test/env.js'], diff --git a/lavamoat/node/policy.json b/lavamoat/node/policy.json index 56892f1bf..d9eef720a 100644 --- a/lavamoat/node/policy.json +++ b/lavamoat/node/policy.json @@ -800,6 +800,26 @@ "through2": true } }, + "@lavamoat/lavapack": { + "builtin": { + "assert": true, + "buffer.Buffer.from": true, + "path.join": true, + "path.relative": true + }, + "globals": { + "__dirname": true, + "process.cwd": true + }, + "packages": { + "JSONStream": true, + "combine-source-map": true, + "convert-source-map": true, + "json-stable-stringify": true, + "through2": true, + "umd": true + } + }, "@nodelib/fs.scandir": { "builtin": { "fs.lstat": true, @@ -1072,6 +1092,13 @@ "pascalcase": true } }, + "bify-module-groups": { + "packages": { + "pify": true, + "pump": true, + "through2": true + } + }, "bl": { "builtin": { "util.inherits": true @@ -2303,26 +2330,6 @@ "through2": true } }, - "gulp-terser-js": { - "builtin": { - "fs.readFileSync": true, - "path.basename": true, - "path.resolve": true - }, - "globals": { - "Buffer.from": true, - "console.error": true, - "console.log": true, - "process.stdout.columns": true - }, - "packages": { - "plugin-error": true, - "source-map": true, - "terser": true, - "through2": true, - "vinyl-sourcemaps-apply": true - } - }, "gulp-watch": { "builtin": { "path.dirname": true, @@ -3702,6 +3709,15 @@ "extend-shallow": true } }, + "squirrelly": { + "builtin": { + "fs.existsSync": true, + "fs.readFileSync": true, + "path.dirname": true, + "path.extname": true, + "path.resolve": true + } + }, "static-eval": { "packages": { "escodegen": true @@ -3921,9 +3937,19 @@ "Buffer.from": true, "atob": true, "btoa": true, - "define": true + "console.log": true, + "console.warn": true, + "define": true, + "process.argv": true, + "process.exit": true, + "process.platform": true, + "process.stderr.write": true, + "process.stdin.on": true, + "process.stdin.resume": true, + "process.stdin.setEncoding": true }, "packages": { + "acorn": true, "source-map": true } }, diff --git a/package.json b/package.json index 70ab3b396..5ec5ba176 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "scripts": { "setup": "yarn install && yarn setup:postinstall", "setup:postinstall": "yarn patch-package && yarn allow-scripts", - "start": "node development/build/index.js dev", + "start": "yarn build:dev dev", "start:lavamoat": "yarn build dev", "dist": "yarn build prod", "build": "lavamoat development/build/index.js", + "build:dev": "node development/build/index.js", "start:test": "yarn build testDev", "benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js", "benchmark:firefox": "SELENIUM_BROWSER=firefox node test/e2e/benchmark.js", @@ -32,11 +33,12 @@ "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --recursive './app/**/*.test.js'", "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'", "test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive", - "test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh", - "test:e2e:chrome:metrics": "SELENIUM_BROWSER=chrome mocha test/e2e/metrics.spec.js", - "test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh", - "test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox mocha test/e2e/metrics.spec.js", + "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", + "test:e2e:chrome:metrics": "SELENIUM_BROWSER=chrome node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js", + "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", + "test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js", "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", + "test:e2e:single": "node test/e2e/run-e2e-test.js", "test:coverage:jest": "jest --coverage --maxWorkers=2", "test:coverage:strict": "nyc --check-coverage yarn test:unit:strict", "test:coverage:path": "nyc --check-coverage yarn test:unit:path", @@ -92,13 +94,16 @@ "3box": "^1.10.2", "@babel/runtime": "^7.5.5", "@download/blockies": "^1.0.3", + "@ensdomains/content-hash": "^2.5.6", + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", "@formatjs/intl-relativetimeformat": "^5.2.6", "@fortawesome/fontawesome-free": "^5.13.0", "@lavamoat/preinstall-always-fail": "^1.0.0", "@material-ui/core": "^4.11.0", - "@metamask/contract-metadata": "^1.26.0", - "@metamask/controllers": "^10.0.0", - "@metamask/eth-ledger-bridge-keyring": "^0.5.0", + "@metamask/contract-metadata": "^1.27.0", + "@metamask/controllers": "^12.0.0", + "@metamask/eth-ledger-bridge-keyring": "^0.6.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", "@metamask/jazzicon": "^2.0.0", @@ -114,10 +119,11 @@ "abortcontroller-polyfill": "^1.4.0", "analytics-node": "^3.4.0-beta.3", "await-semaphore": "^0.1.1", + "base32-encode": "^1.2.0", + "base64-js": "^1.5.1", "bignumber.js": "^4.1.0", "bn.js": "^4.11.7", "classnames": "^2.2.6", - "content-hash": "^2.5.2", "copy-to-clipboard": "^3.0.8", "currency-formatter": "^1.4.2", "debounce-stream": "^2.0.0", @@ -134,10 +140,9 @@ "eth-query": "^2.1.2", "eth-rpc-errors": "^4.0.2", "eth-sig-util": "^3.0.0", - "eth-trezor-keyring": "^0.6.0", + "eth-trezor-keyring": "^0.7.0", "ethereum-ens-network-map": "^1.0.2", "ethereumjs-abi": "^0.6.4", - "ethereumjs-tx": "1.3.7", "ethereumjs-util": "^7.0.10", "ethereumjs-wallet": "^0.6.4", "ethers": "^5.0.8", @@ -188,6 +193,7 @@ "readable-stream": "^2.3.3", "redux": "^4.0.5", "redux-thunk": "^2.3.0", + "requirejs": "^2.3.6", "reselect": "^3.0.1", "rpc-cap": "^3.2.1", "safe-event-emitter": "^1.0.1", @@ -201,6 +207,7 @@ "web3-stream-provider": "^4.0.0" }, "devDependencies": { + "@babel/code-frame": "^7.12.13", "@babel/core": "^7.12.1", "@babel/eslint-parser": "^7.13.14", "@babel/eslint-plugin": "^7.12.1", @@ -213,6 +220,7 @@ "@babel/preset-react": "^7.0.0", "@babel/register": "^7.5.5", "@lavamoat/allow-scripts": "^1.0.6", + "@lavamoat/lavapack": "^1.0.4", "@metamask/auto-changelog": "^2.1.0", "@metamask/eslint-config": "^6.0.0", "@metamask/eslint-config-jest": "^6.0.0", @@ -234,7 +242,10 @@ "@types/react": "^16.9.53", "addons-linter": "1.14.0", "babelify": "^10.0.0", + "bify-module-groups": "^1.0.0", + "bify-vinyl-gator": "^1.0.0", "brfs": "^2.0.2", + "browser-pack": "^6.1.0", "browserify": "^16.5.1", "chalk": "^3.0.0", "chromedriver": "^79.0.0", @@ -244,6 +255,7 @@ "css-loader": "^2.1.1", "css-to-xpath": "^0.1.0", "del": "^3.0.0", + "duplexify": "^4.1.1", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", "eslint": "^7.23.0", @@ -270,7 +282,6 @@ "gulp-rtlcss": "^1.4.0", "gulp-sourcemaps": "^2.6.0", "gulp-stylelint": "^13.0.0", - "gulp-terser-js": "^5.2.2", "gulp-watch": "^5.0.1", "gulp-zip": "^4.0.0", "history": "^5.0.0", @@ -290,6 +301,7 @@ "prettier": "^2.2.1", "prettier-plugin-sort-json": "^0.0.1", "proxyquire": "^2.1.3", + "pumpify": "^2.0.1", "randomcolor": "^0.5.4", "rc": "^1.2.8", "react-devtools": "^4.10.1", @@ -305,15 +317,19 @@ "sinon": "^9.0.0", "source-map": "^0.7.2", "source-map-explorer": "^2.4.2", + "squirrelly": "^8.0.8", "string.prototype.matchall": "^4.0.2", "style-loader": "^0.21.0", "stylelint": "^13.6.1", - "through2": "^2.0.3", + "terser": "^5.7.0", + "through2": "^4.0.2", "ttest": "^2.1.1", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.1", "watchify": "^3.11.1", - "webpack": "^4.41.6" + "webpack": "^4.41.6", + "yargs": "^17.0.1" }, "engines": { "node": "^14.15.1", diff --git a/patches/selenium-webdriver+4.0.0-alpha.7.patch b/patches/selenium-webdriver+4.0.0-alpha.7.patch new file mode 100644 index 000000000..53144785b --- /dev/null +++ b/patches/selenium-webdriver+4.0.0-alpha.7.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/selenium-webdriver/chromium.js b/node_modules/selenium-webdriver/chromium.js +index d828ce5..87176f4 100644 +--- a/node_modules/selenium-webdriver/chromium.js ++++ b/node_modules/selenium-webdriver/chromium.js +@@ -197,6 +197,14 @@ class ServiceBuilder extends remote.DriverService.Builder { + return this.addArguments('--log-path=' + path); + } + ++ /** ++ * Enables Chrome logging. ++ * @returns {!ServiceBuilder} A self reference. ++ */ ++ enableChromeLogging() { ++ return this.addArguments('--enable-chrome-logs'); ++ } ++ + /** + * Enables verbose logging. + * @return {!ServiceBuilder} A self reference. diff --git a/patches/squirrelly+8.0.8.patch b/patches/squirrelly+8.0.8.patch new file mode 100644 index 000000000..f43a50ef5 --- /dev/null +++ b/patches/squirrelly+8.0.8.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/squirrelly/dist/squirrelly.cjs.js b/node_modules/squirrelly/dist/squirrelly.cjs.js +index 7908a34..044e348 100644 +--- a/node_modules/squirrelly/dist/squirrelly.cjs.js ++++ b/node_modules/squirrelly/dist/squirrelly.cjs.js +@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); + // TODO: allow '-' to trim up until newline. Use [^\S\n\r] instead of \s + // TODO: only include trimLeft polyfill if not in ES6 + /* END TYPES */ +-var promiseImpl = new Function('return this')().Promise; ++var promiseImpl = globalThis.Promise; + var asyncFunc = false; + try { + asyncFunc = new Function('return (async function(){}).constructor')(); diff --git a/patches/watchify+3.11.1.patch b/patches/watchify+3.11.1.patch new file mode 100644 index 000000000..cf3e489ad --- /dev/null +++ b/patches/watchify+3.11.1.patch @@ -0,0 +1,38 @@ +diff --git a/node_modules/watchify/index.js b/node_modules/watchify/index.js +index 0753b9f..4fea9e1 100644 +--- a/node_modules/watchify/index.js ++++ b/node_modules/watchify/index.js +@@ -58,33 +58,6 @@ function watchify (b, opts) { + if (pkgcache) pkgcache[file] = pkg; + }); + +- b.on('reset', reset); +- reset(); +- +- function reset () { +- var time = null; +- var bytes = 0; +- b.pipeline.get('record').on('end', function () { +- time = Date.now(); +- }); +- +- b.pipeline.get('wrap').push(through(write, end)); +- function write (buf, enc, next) { +- bytes += buf.length; +- this.push(buf); +- next(); +- } +- function end () { +- var delta = Date.now() - time; +- b.emit('time', delta); +- b.emit('bytes', bytes); +- b.emit('log', bytes + ' bytes written (' +- + (delta / 1000).toFixed(2) + ' seconds)' +- ); +- this.push(null); +- } +- } +- + var fwatchers = {}; + var fwatcherFiles = {}; + var ignoredFiles = {}; diff --git a/shared/constants/gas.js b/shared/constants/gas.js index 6f53aaeae..004e0919b 100644 --- a/shared/constants/gas.js +++ b/shared/constants/gas.js @@ -9,3 +9,32 @@ export const GAS_LIMITS = { // a base estimate for token transfers. BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)), }; + +/** + * These are already declared in @metamask/controllers but importing them from + * that module and re-exporting causes the UI bundle size to expand beyond 4MB + */ +export const GAS_ESTIMATE_TYPES = { + FEE_MARKET: 'fee-market', + LEGACY: 'legacy', + ETH_GASPRICE: 'eth_gasPrice', + NONE: 'none', +}; + +/** + * These represent gas recommendation levels presented in the UI + */ +export const GAS_RECOMMENDATIONS = { + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high', +}; + +/** + * These represent the different edit modes presented in the UI + */ +export const EDIT_GAS_MODES = { + SPEED_UP: 'speed-up', + CANCEL: 'cancel', + MODIFY_IN_PLACE: 'modify-in-place', +}; diff --git a/shared/constants/network.js b/shared/constants/network.js index 1d22b5271..2c4f24467 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -126,6 +126,27 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = { export const INFURA_BLOCKED_KEY = 'countryBlocked'; +/** + * Hardforks are points in the chain where logic is changed significantly + * enough where there is a fork and the new fork becomes the active chain. + * These constants are presented in chronological order starting with BERLIN + * because when we first needed to track the hardfork we had launched support + * for EIP-2718 (where transactions can have types and different shapes) and + * EIP-2930 (optional access lists), which were included in BERLIN. + * + * BERLIN - forked at block number 12,244,000, included typed transactions and + * optional access lists + * LONDON - future, upcoming fork that introduces the baseFeePerGas, an amount + * of the ETH transaction fees that will be burned instead of given to the + * miner. This change necessitated the third type of transaction envelope to + * specify maxFeePerGas and maxPriorityFeePerGas moving the fee bidding system + * to a second price auction model. + */ +export const HARDFORKS = { + BERLIN: 'berlin', + LONDON: 'london', +}; + export const CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP = { [OPTIMISM_CHAIN_ID]: 1, [OPTIMISM_TESTNET_CHAIN_ID]: 1, diff --git a/shared/constants/transaction.js b/shared/constants/transaction.js index 5eedd6086..09d780677 100644 --- a/shared/constants/transaction.js +++ b/shared/constants/transaction.js @@ -60,6 +60,33 @@ export const TRANSACTION_TYPES = { ETH_GET_ENCRYPTION_PUBLIC_KEY: MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY, }; +/** + * In EIP-2718 typed transaction envelopes were specified, with the very first + * typed envelope being 'legacy' and describing the shape of the base + * transaction params that were hitherto the only transaction type sent on + * Ethereum. + * @typedef {Object} TransactionEnvelopeTypes + * @property {'0x0'} LEGACY - A legacy transaction, the very first type. + * @property {'0x1'} ACCESS_LIST - EIP-2930 defined the access list transaction + * type that allowed for specifying the state that a transaction would act + * upon in advance and theoretically save on gas fees. + * @property {'0x2'} FEE_MARKET - The type introduced comes from EIP-1559, + * Fee Market describes the addition of a baseFee to blocks that will be + * burned instead of distributed to miners. Transactions of this type have + * both a maxFeePerGas (maximum total amount in gwei per gas to spend on the + * transaction) which is inclusive of the maxPriorityFeePerGas (maximum amount + * of gwei per gas from the transaction fee to distribute to miner). + */ + +/** + * @type {TransactionEnvelopeTypes} + */ +export const TRANSACTION_ENVELOPE_TYPES = { + LEGACY: '0x0', + ACCESS_LIST: '0x1', + FEE_MARKET: '0x2', +}; + /** * Transaction Status is a mix of Ethereum and MetaMask terminology, used internally * for transaction processing. diff --git a/shared/modules/buffer-utils.test.js b/shared/modules/buffer-utils.test.js index 2f5290cca..f3daa9e17 100644 --- a/shared/modules/buffer-utils.test.js +++ b/shared/modules/buffer-utils.test.js @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import BN from 'bn.js'; import { toBuffer } from './buffer-utils'; @@ -6,64 +5,64 @@ describe('buffer utils', function () { describe('toBuffer', function () { it('should work with prefixed hex strings', function () { const result = toBuffer('0xe'); - assert.equal(result.length, 1); + expect(result).toHaveLength(1); }); it('should work with non prefixed hex strings', function () { const result = toBuffer('e'); - assert.equal(result.length, 1); + expect(result).toHaveLength(1); }); it('should work with weirdly 0x prefixed non-hex strings', function () { const result = toBuffer('0xtest'); - assert.equal(result.length, 6); + expect(result).toHaveLength(6); }); it('should work with regular strings', function () { const result = toBuffer('test'); - assert.equal(result.length, 4); + expect(result).toHaveLength(4); }); it('should work with BN', function () { const result = toBuffer(new BN(100)); - assert.equal(result.length, 1); + expect(result).toHaveLength(1); }); it('should work with Buffer', function () { const result = toBuffer(Buffer.from('test')); - assert.equal(result.length, 4); + expect(result).toHaveLength(4); }); it('should work with a number', function () { const result = toBuffer(100); - assert.equal(result.length, 1); + expect(result).toHaveLength(1); }); it('should work with null or undefined', function () { const result = toBuffer(null); const result2 = toBuffer(undefined); - assert.equal(result.length, 0); - assert.equal(result2.length, 0); + expect(result).toHaveLength(0); + expect(result2).toHaveLength(0); }); it('should work with UInt8Array', function () { const uint8 = new Uint8Array(2); const result = toBuffer(uint8); - assert.equal(result.length, 2); + expect(result).toHaveLength(2); }); it('should work with objects that have a toBuffer property', function () { const result = toBuffer({ toBuffer: () => Buffer.from('hi'), }); - assert.equal(result.length, 2); + expect(result).toHaveLength(2); }); it('should work with objects that have a toArray property', function () { const result = toBuffer({ toArray: () => ['hi'], }); - assert.equal(result.length, 1); + expect(result).toHaveLength(1); }); }); }); diff --git a/ui/helpers/utils/conversion-util.js b/shared/modules/conversion.utils.js similarity index 100% rename from ui/helpers/utils/conversion-util.js rename to shared/modules/conversion.utils.js diff --git a/ui/helpers/utils/conversion-util.test.js b/shared/modules/conversion.utils.test.js similarity index 98% rename from ui/helpers/utils/conversion-util.test.js rename to shared/modules/conversion.utils.test.js index f97640691..d483a9a78 100644 --- a/ui/helpers/utils/conversion-util.test.js +++ b/shared/modules/conversion.utils.test.js @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js'; -import { addCurrencies, conversionUtil } from './conversion-util'; +import { addCurrencies, conversionUtil } from './conversion.utils'; describe('conversion utils', () => { describe('addCurrencies()', () => { diff --git a/shared/modules/fetch-with-timeout.test.js b/shared/modules/fetch-with-timeout.test.js index 2061c2b92..a7400e617 100644 --- a/shared/modules/fetch-with-timeout.test.js +++ b/shared/modules/fetch-with-timeout.test.js @@ -1,6 +1,4 @@ -import { strict as assert } from 'assert'; import nock from 'nock'; - import { MILLISECOND, SECOND } from '../constants/time'; import getFetchWithTimeout from './fetch-with-timeout'; @@ -12,7 +10,7 @@ describe('getFetchWithTimeout', function () { const response = await ( await fetchWithTimeout('https://api.infura.io/money') ).json(); - assert.deepEqual(response, { + expect(response).toStrictEqual({ hodl: false, }); }); @@ -25,14 +23,14 @@ describe('getFetchWithTimeout', function () { const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123); - try { + const fetchWithTimeoutThrowsError = async () => { await fetchWithTimeout('https://api.infura.io/moon').then((r) => r.json(), ); - assert.fail('Request should throw'); - } catch (e) { - assert.ok(e); - } + throw new Error('Request should throw'); + }; + + await expect(fetchWithTimeoutThrowsError()).rejects.toThrow('Aborted'); }); it('should abort the request when the custom timeout is hit', async function () { @@ -43,20 +41,28 @@ describe('getFetchWithTimeout', function () { const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123); - try { + const fetchWithTimeoutThrowsError = async () => { await fetchWithTimeout('https://api.infura.io/moon').then((r) => r.json(), ); - assert.fail('Request should be aborted'); - } catch (e) { - assert.deepEqual(e.message, 'Aborted'); - } + throw new Error('Request should be aborted'); + }; + + await expect(fetchWithTimeoutThrowsError()).rejects.toThrow('Aborted'); }); it('throws on invalid timeout', async function () { - assert.throws(() => getFetchWithTimeout(), 'should throw'); - assert.throws(() => getFetchWithTimeout(-1), 'should throw'); - assert.throws(() => getFetchWithTimeout({}), 'should throw'); - assert.throws(() => getFetchWithTimeout(true), 'should throw'); + expect(() => getFetchWithTimeout()).toThrow( + 'Must specify positive integer timeout.', + ); + expect(() => getFetchWithTimeout(-1)).toThrow( + 'Must specify positive integer timeout.', + ); + expect(() => getFetchWithTimeout({})).toThrow( + 'Must specify positive integer timeout.', + ); + expect(() => getFetchWithTimeout(true)).toThrow( + 'Must specify positive integer timeout.', + ); }); }); diff --git a/shared/modules/gas.utils.js b/shared/modules/gas.utils.js new file mode 100644 index 000000000..ab8c44b36 --- /dev/null +++ b/shared/modules/gas.utils.js @@ -0,0 +1,129 @@ +import { addHexPrefix } from 'ethereumjs-util'; +import { + addCurrencies, + conversionGreaterThan, + multiplyCurrencies, +} from './conversion.utils'; + +/** + * Accepts an options bag containing gas fee parameters in hex format and + * returns a gasTotal parameter representing the maximum amount of wei the + * transaction will cost. + * + * @param {object} options - gas fee parameters object + * @param {string} [options.gasLimit] - the maximum amount of gas to allow this + * transaction to consume. Value is a hex string + * @param {string} [options.gasPrice] - The fee in wei to pay per gas used. + * gasPrice is only set on Legacy type transactions. Value is hex string + * @param {string} [options.maxFeePerGas] - The maximum fee in wei to pay per + * gas used. maxFeePerGas is introduced in EIP 1559 and represents the max + * total a user will pay per gas. Actual cost is determined by baseFeePerGas + * on the block + maxPriorityFeePerGas. Value is hex string + * @returns {string} - The maximum total cost of transaction in hex wei string + */ +export function getMaximumGasTotalInHexWei({ + gasLimit = '0x0', + gasPrice, + maxFeePerGas, +} = {}) { + if (maxFeePerGas) { + return addHexPrefix( + multiplyCurrencies(gasLimit, maxFeePerGas, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); + } + if (!gasPrice) { + throw new Error( + 'getMaximumGasTotalInHexWei requires gasPrice be provided to calculate legacy gas total', + ); + } + return addHexPrefix( + multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); +} + +/** + * Accepts an options bag containing gas fee parameters in hex format and + * returns a gasTotal parameter representing the minimum amount of wei the + * transaction will cost. For gasPrice types this is the same as max. + * + * @param {object} options - gas fee parameters object + * @param {string} [options.gasLimit] - the maximum amount of gas to allow this + * transaction to consume. Value is a hex string + * @param {string} [options.gasPrice] - The fee in wei to pay per gas used. + * gasPrice is only set on Legacy type transactions. Value is hex string + * @param {string} [options.maxFeePerGas] - The maximum fee in wei to pay per + * gas used. maxFeePerGas is introduced in EIP 1559 and represents the max + * total a user will pay per gas. Actual cost is determined by baseFeePerGas + * on the block + maxPriorityFeePerGas. Value is hex string + * @param {string} [options.maxPriorityFeePerGas] - The maximum fee in wei to + * pay a miner to include this transaction. + * @param {string} [options.baseFeePerGas] - The estimated block baseFeePerGas + * that will be burned. Introduced in EIP 1559. Value in hex wei. + * @returns {string} - The minimum total cost of transaction in hex wei string + */ +export function getMinimumGasTotalInHexWei({ + gasLimit = '0x0', + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + baseFeePerGas, +} = {}) { + const isEIP1559Estimate = Boolean( + maxFeePerGas || maxPriorityFeePerGas || baseFeePerGas, + ); + if (isEIP1559Estimate && gasPrice) { + throw new Error( + `getMinimumGasTotalInHexWei expects either gasPrice OR the EIP-1559 gas fields, but both were provided`, + ); + } + + if (isEIP1559Estimate === false && !gasPrice) { + throw new Error( + `getMinimumGasTotalInHexWei expects either gasPrice OR the EIP-1559 gas fields, but neither were provided`, + ); + } + + if (isEIP1559Estimate && !baseFeePerGas) { + throw new Error( + `getMinimumGasTotalInHexWei requires baseFeePerGas be provided when calculating EIP-1559 totals`, + ); + } + + if (isEIP1559Estimate && (!maxFeePerGas || !maxPriorityFeePerGas)) { + throw new Error( + `getMinimumGasTotalInHexWei requires maxFeePerGas and maxPriorityFeePerGas be provided when calculating EIP-1559 totals`, + ); + } + if (isEIP1559Estimate === false) { + return getMaximumGasTotalInHexWei({ gasLimit, gasPrice }); + } + const minimumFeePerGas = addCurrencies(baseFeePerGas, maxPriorityFeePerGas, { + toNumericBase: 'hex', + aBase: 16, + bBase: 16, + }); + + if ( + conversionGreaterThan( + { value: minimumFeePerGas, fromNumericBase: 'hex' }, + { value: maxFeePerGas, fromNumericBase: 'hex' }, + ) + ) { + return getMaximumGasTotalInHexWei({ gasLimit, maxFeePerGas }); + } + return addHexPrefix( + multiplyCurrencies(gasLimit, minimumFeePerGas, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); +} diff --git a/shared/modules/gas.utils.test.js b/shared/modules/gas.utils.test.js new file mode 100644 index 000000000..8c2b9a359 --- /dev/null +++ b/shared/modules/gas.utils.test.js @@ -0,0 +1,131 @@ +const { addHexPrefix } = require('ethereumjs-util'); +const { conversionUtil } = require('./conversion.utils'); +const { + getMaximumGasTotalInHexWei, + getMinimumGasTotalInHexWei, +} = require('./gas.utils'); + +const feesToTest = [10, 24, 90]; +const tipsToTest = [2, 10, 50]; +const baseFeesToTest = [8, 12, 24]; +const gasLimitsToTest = [21000, 100000]; + +describe('gas utils', () => { + describe('when using EIP 1559 fields', () => { + describe('getMaximumGasTotalInHexWei', () => { + feesToTest.forEach((maxFeePerGas) => { + describe(`when maxFeePerGas is ${maxFeePerGas}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * maxFeePerGas).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + const result = conversionUtil( + getMaximumGasTotalInHexWei({ + gasLimit: gasLimitHex, + maxFeePerGas: addHexPrefix(maxFeePerGas.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ); + it(`returns ${expectedResult} when provided gasLimit: ${gasLimit}`, () => { + expect(result).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + + describe('getMinimumGasTotalInHexWei', () => { + feesToTest.forEach((maxFeePerGas) => { + tipsToTest.forEach((maxPriorityFeePerGas) => { + baseFeesToTest.forEach((baseFeePerGas) => { + describe(`when baseFee is ${baseFeePerGas}, maxFeePerGas is ${maxFeePerGas} and tip is ${maxPriorityFeePerGas}`, () => { + const maximum = maxFeePerGas; + const minimum = baseFeePerGas + maxPriorityFeePerGas; + const expectedEffectiveGasPrice = + minimum < maximum ? minimum : maximum; + const results = gasLimitsToTest.map((gasLimit) => { + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + const result = conversionUtil( + getMinimumGasTotalInHexWei({ + gasLimit: gasLimitHex, + maxFeePerGas: addHexPrefix(maxFeePerGas.toString(16)), + maxPriorityFeePerGas: addHexPrefix( + maxPriorityFeePerGas.toString(16), + ), + baseFeePerGas: addHexPrefix(baseFeePerGas.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ); + return { result, gasLimit }; + }); + it(`should use an effective gasPrice of ${expectedEffectiveGasPrice}`, () => { + expect( + results.every(({ result, gasLimit }) => { + const effectiveGasPrice = Number(result) / gasLimit; + return effectiveGasPrice === expectedEffectiveGasPrice; + }), + ).toBe(true); + }); + results.forEach(({ result, gasLimit }) => { + const expectedResult = ( + expectedEffectiveGasPrice * gasLimit + ).toString(); + it(`returns ${expectedResult} when provided gasLimit: ${gasLimit}`, () => { + expect(result).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + }); + }); + }); + + describe('when using legacy fields', () => { + describe('getMaximumGasTotalInHexWei', () => { + feesToTest.forEach((gasPrice) => { + describe(`when gasPrice is ${gasPrice}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * gasPrice).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + it(`returns ${expectedResult} when provided gasLimit of ${gasLimit}`, () => { + expect( + conversionUtil( + getMaximumGasTotalInHexWei({ + gasLimit: gasLimitHex, + gasPrice: addHexPrefix(gasPrice.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ), + ).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + + describe('getMinimumGasTotalInHexWei', () => { + feesToTest.forEach((gasPrice) => { + describe(`when gasPrice is ${gasPrice}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * gasPrice).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + it(`returns ${expectedResult} when provided gasLimit of ${gasLimit}`, () => { + expect( + conversionUtil( + getMinimumGasTotalInHexWei({ + gasLimit: gasLimitHex, + gasPrice: addHexPrefix(gasPrice.toString(16)), + }), + { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }, + ), + ).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + }); +}); diff --git a/shared/modules/hexstring-utils.test.js b/shared/modules/hexstring-utils.test.js index 3d292451e..bf2d80116 100644 --- a/shared/modules/hexstring-utils.test.js +++ b/shared/modules/hexstring-utils.test.js @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import { toChecksumAddress } from 'ethereumjs-util'; import { isValidHexAddress } from './hexstring-utils'; @@ -7,51 +6,51 @@ describe('hexstring utils', function () { it('should allow 40-char non-prefixed hex', function () { const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = isValidHexAddress(address); - assert.equal(result, true); + expect(result).toBe(true); }); it('should allow 42-char prefixed hex', function () { const address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = isValidHexAddress(address); - assert.equal(result, true); + expect(result).toBe(true); }); it('should NOT allow 40-char non-prefixed hex when allowNonPrefixed is false', function () { const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = isValidHexAddress(address, { allowNonPrefixed: false }); - assert.equal(result, false); + expect(result).toBe(false); }); it('should NOT allow any length of non hex-prefixed string', function () { const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85'; const result = isValidHexAddress(address); - assert.equal(result, false); + expect(result).toBe(false); }); it('should NOT allow less than 42 character hex-prefixed string', function () { const address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85'; const result = isValidHexAddress(address); - assert.equal(result, false); + expect(result).toBe(false); }); it('should recognize correct capitalized checksum', function () { const address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825'; const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); - assert.equal(result, true); + expect(result).toBe(true); }); it('should recognize incorrect capitalized checksum', function () { const address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825'; const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); - assert.equal(result, false); + expect(result).toBe(false); }); it('should recognize this sample hashed address', function () { const address = '0x5Fda30Bb72B8Dfe20e48A00dFc108d0915BE9Bb0'; const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); const hashed = toChecksumAddress(address.toLowerCase()); - assert.equal(hashed, address); - assert.equal(result, true); + expect(hashed).toBe(address); + expect(result).toBe(true); }); }); }); diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index 092f8e465..8e0e167af 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -16,8 +16,8 @@ export function transactionMatchesNetwork(transaction, chainId, networkId) { */ export function isEIP1559Transaction(transaction) { return ( - isHexString(transaction.txParams.maxFeePerGas) && - isHexString(transaction.txParams.maxPriorityFeePerGas) + isHexString(transaction?.txParams?.maxFeePerGas) && + isHexString(transaction?.txParams?.maxPriorityFeePerGas) ); } diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index 9c3b7db00..fc8843c58 100644 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -2,7 +2,11 @@ const path = require('path'); const { promises: fs, constants: fsConstants } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); const ttest = require('ttest'); +const { retry } = require('../../development/lib/retry'); +const { exitWithError } = require('../../development/lib/exit-with-error'); const { withFixtures } = require('./helpers'); const { PAGES } = require('./webdriver/driver'); @@ -38,6 +42,9 @@ const minResult = calculateResult((array) => Math.min(...array)); const maxResult = calculateResult((array) => Math.max(...array)); const averageResult = calculateResult((array) => calculateAverage(array)); const standardDeviationResult = calculateResult((array) => { + if (array.length === 1) { + return 0; + } const average = calculateAverage(array); const squareDiffs = array.map((value) => Math.pow(value - average, 2)); return Math.sqrt(calculateAverage(squareDiffs)); @@ -46,15 +53,19 @@ const standardDeviationResult = calculateResult((array) => { const calculateMarginOfError = (array) => ttest(array).confidence()[1] - calculateAverage(array); const marginOfErrorResult = calculateResult((array) => - calculateMarginOfError(array), + array.length === 1 ? 0 : calculateMarginOfError(array), ); -async function profilePageLoad(pages, numSamples) { +async function profilePageLoad(pages, numSamples, retries) { const results = {}; for (const pageName of pages) { const runResults = []; for (let i = 0; i < numSamples; i += 1) { - runResults.push(await measurePage(pageName)); + let result; + await retry(retries, async () => { + result = await measurePage(pageName); + }); + runResults.push(result); } if (runResults.some((result) => result.navigation.lenth > 1)) { @@ -126,66 +137,63 @@ async function getFirstParentDirectoryThatExists(directory) { } async function main() { - const args = process.argv.slice(2); + const { argv } = yargs(hideBin(process.argv)).usage( + '$0 [options]', + 'Run a page load benchmark', + (_yargs) => + _yargs + .option('pages', { + array: true, + default: ['home'], + description: + 'Set the page(s) to be benchmarked. This flag can accept multiple values (space-separated).', + choices: ALL_PAGES, + }) + .option('samples', { + default: DEFAULT_NUM_SAMPLES, + description: 'The number of times the benchmark should be run.', + type: 'number', + }) + .option('out', { + description: + 'Output filename. Output printed to STDOUT of this is omitted.', + type: 'string', + normalize: true, + }) + .option('retries', { + default: 0, + description: + 'Set how many times each benchmark sample should be retried upon failure.', + type: 'number', + }), + ); + + const { pages, samples, out, retries } = argv; - let pages = ['home']; - let numSamples = DEFAULT_NUM_SAMPLES; - let outputPath; let outputDirectory; let existingParentDirectory; - - while (args.length) { - if (/^(--pages|-p)$/u.test(args[0])) { - if (args[1] === undefined) { - throw new Error('Missing pages argument'); - } - pages = args[1].split(','); - for (const page of pages) { - if (!ALL_PAGES.includes(page)) { - throw new Error(`Invalid page: '${page}`); - } - } - args.splice(0, 2); - } else if (/^(--samples|-s)$/u.test(args[0])) { - if (args[1] === undefined) { - throw new Error('Missing number of samples'); - } - numSamples = parseInt(args[1], 10); - if (isNaN(numSamples)) { - throw new Error(`Invalid 'samples' argument given: '${args[1]}'`); - } - args.splice(0, 2); - } else if (/^(--out|-o)$/u.test(args[0])) { - if (args[1] === undefined) { - throw new Error('Missing output filename'); - } - outputPath = path.resolve(args[1]); - outputDirectory = path.dirname(outputPath); - existingParentDirectory = await getFirstParentDirectoryThatExists( - outputDirectory, - ); - if (!(await isWritable(existingParentDirectory))) { - throw new Error(`Specified directory is not writable: '${args[1]}'`); - } - args.splice(0, 2); - } else { - throw new Error(`Unrecognized argument: '${args[0]}'`); + if (out) { + outputDirectory = path.dirname(out); + existingParentDirectory = await getFirstParentDirectoryThatExists( + outputDirectory, + ); + if (!(await isWritable(existingParentDirectory))) { + throw new Error('Specified output file directory is not writable'); } } - const results = await profilePageLoad(pages, numSamples); + const results = await profilePageLoad(pages, samples, retries); - if (outputPath) { + if (out) { if (outputDirectory !== existingParentDirectory) { await fs.mkdir(outputDirectory, { recursive: true }); } - await fs.writeFile(outputPath, JSON.stringify(results, null, 2)); + await fs.writeFile(out, JSON.stringify(results, null, 2)); } else { console.log(JSON.stringify(results, null, 2)); } } -main().catch((e) => { - console.error(e); - process.exit(1); +main().catch((error) => { + exitWithError(error); }); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index ae8b4d6c5..cde3b0243 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -33,6 +33,7 @@ async function withFixtures(options, testSuite) { let segmentStub; let webDriver; + let failed = false; try { await ganacheServer.start(ganacheOptions); if (ganacheOptions?.concurrent) { @@ -103,6 +104,7 @@ async function withFixtures(options, testSuite) { } } } catch (error) { + failed = true; if (webDriver) { try { await webDriver.verboseReportOnFailure(title); @@ -112,26 +114,28 @@ async function withFixtures(options, testSuite) { } throw error; } finally { - await fixtureServer.stop(); - await ganacheServer.quit(); - if (ganacheOptions?.concurrent) { - await secondaryGanacheServer.quit(); - } - if (webDriver) { - await webDriver.quit(); - } - if (dappServer) { - await new Promise((resolve, reject) => { - dappServer.close((error) => { - if (error) { - return reject(error); - } - return resolve(); + if (!failed || process.env.E2E_LEAVE_RUNNING !== 'true') { + await fixtureServer.stop(); + await ganacheServer.quit(); + if (ganacheOptions?.concurrent) { + await secondaryGanacheServer.quit(); + } + if (webDriver) { + await webDriver.quit(); + } + if (dappServer) { + await new Promise((resolve, reject) => { + dappServer.close((error) => { + if (error) { + return reject(error); + } + return resolve(); + }); }); - }); - } - if (segmentServer) { - await segmentServer.stop(); + } + if (segmentServer) { + await segmentServer.stop(); + } } } } diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index b813a597d..0544fb5e2 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1,24 +1,44 @@ const { strict: assert } = require('assert'); +const path = require('path'); const enLocaleMessages = require('../../app/_locales/en/messages.json'); +const createStaticServer = require('../../development/create-static-server'); const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers'); const { buildWebDriver } = require('./webdriver'); const Ganache = require('./ganache'); const ganacheServer = new Ganache(); +const dappPort = 8080; describe('MetaMask', function () { let driver; + let dappServer; let tokenAddress; const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'; - this.timeout(0); this.bail(true); + let failed = false; + before(async function () { await ganacheServer.start(); + const dappDirectory = path.resolve( + __dirname, + '..', + '..', + 'node_modules', + '@metamask', + 'test-dapp', + 'dist', + ); + dappServer = createStaticServer(dappDirectory); + dappServer.listen(dappPort); + await new Promise((resolve, reject) => { + dappServer.on('listening', resolve); + dappServer.on('error', reject); + }); const result = await buildWebDriver(); driver = result.driver; await driver.navigate(); @@ -36,13 +56,25 @@ describe('MetaMask', function () { } } if (this.currentTest.state === 'failed') { + failed = true; await driver.verboseReportOnFailure(this.currentTest.title); } }); after(async function () { + if (process.env.E2E_LEAVE_RUNNING === 'true' && failed) { + return; + } await ganacheServer.quit(); await driver.quit(); + await new Promise((resolve, reject) => { + dappServer.close((error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + }); }); describe('Going through the first time flow', function () { @@ -174,50 +206,6 @@ describe('MetaMask', function () { }); }); - describe('Lock an unlock', function () { - it('logs out of the account', async function () { - await driver.clickElement('.account-menu__icon'); - await driver.delay(regularDelayMs); - - const lockButton = await driver.findClickableElement( - '.account-menu__lock-button', - ); - assert.equal(await lockButton.getText(), 'Lock'); - await lockButton.click(); - await driver.delay(regularDelayMs); - }); - - it('accepts the account password after lock', async function () { - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); - await driver.delay(largeDelayMs * 4); - }); - }); - - describe('Add account', function () { - it('choose Create Account from the account menu', async function () { - await driver.clickElement('.account-menu__icon'); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Create Account', tag: 'div' }); - await driver.delay(regularDelayMs); - }); - - it('set account name', async function () { - await driver.fill('.new-account-create-form input', '2nd account'); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Create', tag: 'button' }); - await driver.delay(largeDelayMs); - }); - - it('should display correct account name', async function () { - const accountName = await driver.findElement('.selected-account__name'); - assert.equal(await accountName.getText(), '2nd account'); - await driver.delay(regularDelayMs); - }); - }); - describe('Import Secret Recovery Phrase', function () { it('logs out of the vault', async function () { await driver.clickElement('.account-menu__icon'); @@ -265,223 +253,11 @@ describe('MetaMask', function () { }); }); - describe('Send ETH from inside MetaMask using default gas', function () { - it('starts a send transaction', async function () { - await driver.clickElement('[data-testid="eth-overview-send"]'); - await driver.delay(regularDelayMs); - - await driver.fill( - 'input[placeholder="Search, public address (0x), or ENS"]', - '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - ); - - const inputAmount = await driver.findElement('.unit-input__input'); - await inputAmount.fill('1000'); - - const errorAmount = await driver.findElement('.send-v2__error-amount'); - assert.equal( - await errorAmount.getText(), - 'Insufficient funds.', - 'send screen should render an insufficient fund error message', - ); - - await inputAmount.press(driver.Key.BACK_SPACE); - await driver.delay(50); - await inputAmount.press(driver.Key.BACK_SPACE); - await driver.delay(50); - await inputAmount.press(driver.Key.BACK_SPACE); - await driver.delay(tinyDelayMs); - - await driver.assertElementNotPresent('.send-v2__error-amount'); - - const amountMax = await driver.findClickableElement( - '.send-v2__amount-max', - ); - await amountMax.click(); - - let inputValue = await inputAmount.getAttribute('value'); - - assert(Number(inputValue) > 99); - - await amountMax.click(); - - assert.equal(await inputAmount.isEnabled(), true); - - await inputAmount.fill('1'); - - inputValue = await inputAmount.getAttribute('value'); - assert.equal(inputValue, '1'); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(regularDelayMs); - }); - - it('confirms the transaction', async function () { - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(largeDelayMs * 2); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.clickElement('[data-testid="home__activity-tab"]'); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 1; - }, 10000); - - await driver.waitForSelector({ - css: '.transaction-list-item__primary-currency', - text: '-1 ETH', - }); - }); - }); - - describe('Send ETH from inside MetaMask using fast gas option', function () { - it('starts a send transaction', async function () { - await driver.clickElement('[data-testid="eth-overview-send"]'); - await driver.delay(regularDelayMs); - - await driver.fill( - 'input[placeholder="Search, public address (0x), or ENS"]', - '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - ); - - const inputAmount = await driver.findElement('.unit-input__input'); - await inputAmount.fill('1'); - - const inputValue = await inputAmount.getAttribute('value'); - assert.equal(inputValue, '1'); - - // Set the gas price - await driver.clickElement({ text: 'Fast', tag: 'button/div/div' }); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(regularDelayMs); - }); - - it('confirms the transaction', async function () { - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(largeDelayMs); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.waitForSelector( - '.transaction-list__completed-transactions .transaction-list-item:nth-child(2)', - ); - await driver.waitForSelector({ - css: '.transaction-list-item__primary-currency', - text: '-1 ETH', - }); - }); - }); - - describe('Send ETH from inside MetaMask using advanced gas modal', function () { - it('starts a send transaction', async function () { - await driver.clickElement('[data-testid="eth-overview-send"]'); - await driver.delay(regularDelayMs); - - await driver.fill( - 'input[placeholder="Search, public address (0x), or ENS"]', - '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - ); - - const inputAmount = await driver.findElement('.unit-input__input'); - await inputAmount.fill('1'); - - const inputValue = await inputAmount.getAttribute('value'); - assert.equal(inputValue, '1'); - - // Set the gas limit - await driver.clickElement('.advanced-gas-options-btn'); - await driver.delay(regularDelayMs); - - // wait for gas modal to be visible - const gasModal = await driver.findVisibleElement('span .modal'); - - await driver.clickElement({ text: 'Save', tag: 'button' }); - - // Wait for gas modal to be removed from DOM - await gasModal.waitForElementState('hidden'); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(regularDelayMs); - }); - - it('confirms the transaction', async function () { - const transactionAmounts = await driver.findElements( - '.currency-display-component__text', - ); - const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '1'); - - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(largeDelayMs); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 3; - }, 10000); - - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-1 ETH', - }, - { timeout: 10000 }, - ); - }); - }); - - describe('Send ETH from dapp using advanced gas controls', function () { + describe('Navigate transactions', function () { let windowHandles; let extension; let popup; let dapp; - - it('goes to the settings screen', async function () { - await driver.clickElement('.account-menu__icon'); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Settings', tag: 'div' }); - - // await driver.findElement('.tab-bar') - - await driver.clickElement({ text: 'Advanced', tag: 'div' }); - await driver.delay(regularDelayMs); - - await driver.clickElement( - '[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > div > div', - ); - - const advancedGasTitle = await driver.findElement({ - text: 'Advanced gas controls', - tag: 'span', - }); - await driver.scrollToElement(advancedGasTitle); - - await driver.clickElement( - '[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > div > div', - ); - windowHandles = await driver.getAllWindowHandles(); - extension = windowHandles[0]; - await driver.closeAllWindowHandlesExcept([extension]); - - await driver.clickElement('.app-header__logo-container'); - - await driver.delay(largeDelayMs); - }); - it('connects the dapp', async function () { await driver.openNewPage('http://127.0.0.1:8080/'); await driver.delay(regularDelayMs); @@ -514,86 +290,7 @@ describe('MetaMask', function () { await driver.delay(regularDelayMs); }); - it('initiates a send from the dapp', async function () { - await driver.clickElement({ text: 'Send', tag: 'button' }, 10000); - await driver.delay(2000); - - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.delay(regularDelayMs); - - await driver.assertElementNotPresent({ text: 'Data', tag: 'li' }); - - const [gasPriceInput, gasLimitInput] = await driver.findElements( - '.advanced-gas-inputs__gas-edit-row__input', - ); - - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.fill('10'); - await driver.delay(50); - await driver.delay(tinyDelayMs); - await driver.delay(50); - - await gasLimitInput.fill(''); - await driver.delay(50); - await gasLimitInput.fill('25000'); - - await driver.delay(1000); - - await driver.clickElement({ text: 'Confirm', tag: 'button' }, 10000); - await driver.delay(regularDelayMs); - - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); - await driver.delay(regularDelayMs); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 4; - }, 10000); - - await driver.waitForSelector({ - css: '.transaction-list-item__primary-currency', - text: '-3 ETH', - }); - }); - - it('the transaction has the expected gas price', async function () { - const txValue = await driver.findClickableElement( - '.transaction-list-item__primary-currency', - ); - await txValue.click(); - const popoverCloseButton = await driver.findClickableElement( - '.popover-header__button', - ); - await driver.waitForSelector({ - css: '[data-testid="transaction-breakdown__gas-price"]', - text: '10', - }); - await popoverCloseButton.click(); - }); - }); - - describe('Navigate transactions', function () { it('adds multiple transactions', async function () { - await driver.delay(regularDelayMs); - - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - const dapp = windowHandles[1]; - - await driver.switchToWindow(dapp); - await driver.delay(largeDelayMs); - const send3eth = await driver.findClickableElement({ text: 'Send', tag: 'button', @@ -616,6 +313,7 @@ describe('MetaMask', function () { await driver.switchToWindow(extension); await driver.delay(regularDelayMs); + await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.clickElement('.transaction-list-item'); await driver.delay(largeDelayMs); }); @@ -710,10 +408,6 @@ describe('MetaMask', function () { 'second transaction in focus', ); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - const dapp = windowHandles[1]; - await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); @@ -772,200 +466,8 @@ describe('MetaMask', function () { const confirmedTxes = await driver.findElements( '.transaction-list__completed-transactions .transaction-list-item', ); - return confirmedTxes.length === 5; - }, 10000); - }); - }); - - describe('Deploy contract and call contract methods', function () { - let extension; - let dapp; - it('creates a deploy contract transaction', async function () { - const windowHandles = await driver.getAllWindowHandles(); - extension = windowHandles[0]; - dapp = windowHandles[1]; - await driver.delay(tinyDelayMs); - - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - - await driver.clickElement('#deployButton'); - await driver.delay(regularDelayMs); - - await driver.switchToWindow(extension); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Contract Deployment', tag: 'h2' }); - await driver.delay(largeDelayMs); - }); - - it('displays the contract creation data', async function () { - await driver.clickElement({ text: 'Data', tag: 'button' }); - await driver.delay(regularDelayMs); - - await driver.findElement({ text: '127.0.0.1', tag: 'div' }); - - const confirmDataDiv = await driver.findElement( - '.confirm-page-container-content__data-box', - ); - const confirmDataText = await confirmDataDiv.getText(); - assert.ok(confirmDataText.includes('Origin:')); - assert.ok(confirmDataText.includes('127.0.0.1')); - assert.ok(confirmDataText.includes('Bytes:')); - assert.ok(confirmDataText.includes('675')); - - await driver.clickElement({ text: 'Details', tag: 'button' }); - await driver.delay(regularDelayMs); - }); - - it('confirms a deploy contract transaction', async function () { - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(largeDelayMs); - - await driver.waitForSelector( - '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(6)', - ); - - await driver.waitForSelector( - { - css: '.list-item__title', - text: 'Contract Deployment', - }, - { timeout: 10000 }, - ); - await driver.delay(regularDelayMs); - }); - - it('calls and confirms a contract method where ETH is sent', async function () { - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - - await driver.waitForSelector( - { - css: '#contractStatus', - text: 'Deployed', - }, - { timeout: 15000 }, - ); - - await driver.clickElement('#depositButton'); - await driver.delay(largeDelayMs); - - await driver.waitForSelector( - { - css: '#contractStatus', - text: 'Deposit initiated', - }, - { timeout: 10000 }, - ); - - await driver.switchToWindow(extension); - await driver.delay(largeDelayMs * 2); - - await driver.findElements('.transaction-list-item--unconfirmed'); - const txListValue = await driver.findClickableElement( - '.transaction-list-item__primary-currency', - ); - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-4 ETH', - }, - { timeout: 10000 }, - ); - await txListValue.click(); - await driver.delay(regularDelayMs); - - // Set the gas limit - await driver.clickElement('.confirm-detail-row__header-text--edit'); - // wait for gas modal to be visible. - const gasModal = await driver.findVisibleElement('span .modal'); - await driver.clickElement('.page-container__tab:nth-of-type(2)'); - await driver.delay(regularDelayMs); - - const [gasPriceInput, gasLimitInput] = await driver.findElements( - '.advanced-gas-inputs__gas-edit-row__input', - ); - const gasLimitValue = await gasLimitInput.getAttribute('value'); - assert(Number(gasLimitValue) < 100000, 'Gas Limit too high'); - - await gasPriceInput.fill('10'); - await driver.delay(50); - - await gasLimitInput.fill('60001'); - - await driver.delay(1000); - - await driver.clickElement({ text: 'Save', tag: 'button' }); - - // wait for gas modal to be detached from DOM - await gasModal.waitForElementState('hidden'); - - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(regularDelayMs); - - await driver.waitForSelector( - '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(7)', - { timeout: 10000 }, - ); - await driver.waitForSelector( - { - css: - '.transaction-list__completed-transactions .transaction-list-item__primary-currency', - text: '-4 ETH', - }, - { timeout: 10000 }, - ); - }); - - it('calls and confirms a contract method where ETH is received', async function () { - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - - await driver.clickElement('#withdrawButton'); - await driver.delay(regularDelayMs); - - await driver.switchToWindow(extension); - await driver.delay(largeDelayMs * 2); - - await driver.clickElement( - '.transaction-list__pending-transactions .transaction-list-item', - ); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(regularDelayMs); - - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 8; + return confirmedTxes.length === 1; }, 10000); - - await driver.waitForSelector( - { - css: '.transaction-list-item__primary-currency', - text: '-0 ETH', - }, - { timeout: 10000 }, - ); - - await driver.closeAllWindowHandlesExcept([extension, dapp]); - await driver.switchToWindow(extension); - }); - - it('renders the correct ETH balance', async function () { - const balance = await driver.waitForSelector( - { - css: '[data-testid="eth-overview__primary-currency"]', - text: '87.', - }, - { timeout: 10000 }, - ); - const tokenAmount = await balance.getText(); - assert.ok(/^87.*\s*ETH.*$/u.test(tokenAmount)); - await driver.delay(regularDelayMs); }); }); diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js new file mode 100644 index 000000000..a73021bd4 --- /dev/null +++ b/test/e2e/run-all.js @@ -0,0 +1,57 @@ +const path = require('path'); +const { promises: fs } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const { runInShell } = require('../../development/lib/run-command'); +const { exitWithError } = require('../../development/lib/exit-with-error'); + +async function main() { + const { argv } = yargs(hideBin(process.argv)) + .usage( + '$0 [options]', + 'Run all E2E tests, with a variable number of retries.', + (_yargs) => + _yargs + .option('browser', { + description: `Set the browser used; either 'chrome' or 'firefox'.`, + type: 'string', + choices: ['chrome', 'firefox'], + }) + .option('retries', { + description: + 'Set how many times the test should be retried upon failure.', + type: 'number', + }), + ) + .strict() + .help('help'); + + const { browser, retries } = argv; + + const testDir = path.join(__dirname, 'tests'); + const metamaskUiTest = path.join(__dirname, 'metamask-ui.spec.js'); + + const testFilenames = await fs.readdir(testDir); + const testPaths = testFilenames.map((filename) => + path.join(testDir, filename), + ); + const allE2eTestPaths = [...testPaths, metamaskUiTest]; + + const runE2eTestPath = path.join(__dirname, 'run-e2e-test.js'); + + const args = [runE2eTestPath]; + if (browser) { + args.push('--browser', browser); + } + if (retries) { + args.push('--retries', retries); + } + + for (const testPath of allE2eTestPaths) { + await runInShell('node', [...args, testPath]); + } +} + +main().catch((error) => { + exitWithError(error); +}); diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 18c3443c9..88cc695d8 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -5,33 +5,11 @@ set -e set -u set -o pipefail -retry () { - retry=0 - limit="${METAMASK_E2E_RETRY_LIMIT:-3}" - while [[ $retry -lt $limit ]] - do - "$@" && break - retry=$(( retry + 1 )) - sleep 1 - done +readonly __DIR__=$( cd "${BASH_SOURCE[0]%/*}" && pwd ) - if [[ $retry == "$limit" ]] - then - exit 1 - fi -} - -export PATH="$PATH:./node_modules/.bin" - -for spec in test/e2e/tests/*.spec.js +for spec in "${__DIR__}"/tests/*.spec.js do - retry mocha --no-timeouts "${spec}" + node "${__DIR__}/run-e2e-test.js" "${spec}" done -retry concurrently --kill-others \ - --names 'dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/metamask-ui.spec' - +node "${__DIR__}/run-e2e-test.js" "${__DIR__}/metamask-ui.spec.js" diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js new file mode 100644 index 000000000..ce349ad02 --- /dev/null +++ b/test/e2e/run-e2e-test.js @@ -0,0 +1,83 @@ +const { promises: fs } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const { runInShell } = require('../../development/lib/run-command'); +const { exitWithError } = require('../../development/lib/exit-with-error'); +const { retry } = require('../../development/lib/retry'); + +async function main() { + const { argv } = yargs(hideBin(process.argv)) + .usage( + '$0 [options] ', + 'Run a single E2E test, with a variable number of retries.', + (_yargs) => + _yargs + .option('browser', { + default: process.env.SELENIUM_BROWSER, + description: `Set the browser used; either 'chrome' or 'firefox'.`, + type: 'string', + choices: ['chrome', 'firefox'], + }) + .option('retries', { + default: 0, + description: + 'Set how many times the test should be retried upon failure.', + type: 'number', + }) + .option('leave-running', { + default: false, + description: + 'Leaves the browser running after a test fails, along with anything else that the test used (ganache, the test dapp, etc.)', + type: 'boolean', + }) + .positional('e2e-test-path', { + describe: 'The path for the E2E test to run.', + type: 'string', + normalize: true, + }), + ) + .strict() + .help('help'); + + const { browser, e2eTestPath, retries, leaveRunning } = argv; + + if (!browser) { + exitWithError( + `"The browser must be set, via the '--browser' flag or the SELENIUM_BROWSER environment variable`, + ); + return; + } else if (browser !== process.env.SELENIUM_BROWSER) { + process.env.SELENIUM_BROWSER = browser; + } + + try { + const stat = await fs.stat(e2eTestPath); + if (!stat.isFile()) { + exitWithError('Test path must be a file'); + return; + } + } catch (error) { + if (error.code === 'ENOENT') { + exitWithError('Test path specified does not exist'); + return; + } else if (error.code === 'EACCES') { + exitWithError( + 'Access to test path is forbidden by file access permissions', + ); + return; + } + throw error; + } + + if (leaveRunning) { + process.env.E2E_LEAVE_RUNNING = 'true'; + } + + await retry(retries, async () => { + await runInShell('yarn', ['mocha', '--no-timeouts', e2eTestPath]); + }); +} + +main().catch((error) => { + exitWithError(error); +}); diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js new file mode 100644 index 000000000..910402e43 --- /dev/null +++ b/test/e2e/tests/add-account.spec.js @@ -0,0 +1,39 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Add account', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('should display correct new account name after create', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Create Account', tag: 'div' }); + await driver.fill('.new-account-create-form input', '2nd account'); + await driver.clickElement({ text: 'Create', tag: 'button' }); + + const accountName = await driver.waitForSelector({ + css: '.selected-account__name', + text: '2nd', + }); + assert.equal(await accountName.getText(), '2nd account'); + }, + ); + }); +}); diff --git a/test/e2e/tests/contract-interactions.spec.js b/test/e2e/tests/contract-interactions.spec.js new file mode 100644 index 000000000..a88a12032 --- /dev/null +++ b/test/e2e/tests/contract-interactions.spec.js @@ -0,0 +1,139 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Deploy contract and call contract methods', function () { + let windowHandles; + let extension; + let popup; + let dapp; + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('should display the correct account balance after contract interactions', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // connects the dapp + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); + extension = windowHandles[0]; + dapp = await driver.switchToWindowWithTitle( + 'E2E Test Dapp', + windowHandles, + ); + popup = windowHandles.find( + (handle) => handle !== extension && handle !== dapp, + ); + await driver.switchToWindow(popup); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + + // creates a deploy contract transaction + await driver.switchToWindow(dapp); + await driver.clickElement('#deployButton'); + + // displays the contract creation data + await driver.switchToWindow(extension); + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.clickElement({ text: 'Contract Deployment', tag: 'h2' }); + await driver.clickElement({ text: 'Data', tag: 'button' }); + await driver.findElement({ text: '127.0.0.1', tag: 'div' }); + const confirmDataDiv = await driver.findElement( + '.confirm-page-container-content__data-box', + ); + const confirmDataText = await confirmDataDiv.getText(); + assert.ok(confirmDataText.includes('Origin:')); + assert.ok(confirmDataText.includes('127.0.0.1')); + assert.ok(confirmDataText.includes('Bytes:')); + assert.ok(confirmDataText.includes('675')); + + // confirms a deploy contract transaction + await driver.clickElement({ text: 'Details', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', + { timeout: 10000 }, + ); + const completedTx = await driver.findElement('.list-item__title'); + const completedTxText = await completedTx.getText(); + assert.equal(completedTxText, 'Contract Deployment'); + + // calls and confirms a contract method where ETH is sent + await driver.switchToWindow(dapp); + await driver.clickElement('#depositButton'); + await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.switchToWindow(extension); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', + { timeout: 10000 }, + ); + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-4 ETH', + }, + { timeout: 10000 }, + ); + + // calls and confirms a contract method where ETH is received + await driver.switchToWindow(dapp); + await driver.clickElement('#withdrawButton'); + await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.switchToWindow(extension); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(3)', + { timeout: 10000 }, + ); + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-0 ETH', + }, + { timeout: 10000 }, + ); + + // renders the correct ETH balance + await driver.switchToWindow(extension); + const balance = await driver.waitForSelector( + { + css: '[data-testid="eth-overview__primary-currency"]', + text: '21.', + }, + { timeout: 10000 }, + ); + const tokenAmount = await balance.getText(); + assert.ok(/^21.*\s*ETH.*$/u.test(tokenAmount)); + }, + ); + }); +}); diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js index a07f44c96..185a0d8c0 100644 --- a/test/e2e/tests/incremental-security.spec.js +++ b/test/e2e/tests/incremental-security.spec.js @@ -122,7 +122,7 @@ describe('Incremental Security', function () { // should show a backup reminder const backupReminder = await driver.findElements({ xpath: - "//div[contains(@class, 'home-notification__text') and contains(text(), 'Backup your Secret Recovery code to keep your wallet and funds secure')]", + "//div[contains(@class, 'home-notification__text') and contains(text(), 'Backup your Secret Recovery Phrase to keep your wallet and funds secure')]", }); assert.equal(backupReminder.length, 1); diff --git a/test/e2e/tests/lock-account.spec.js b/test/e2e/tests/lock-account.spec.js new file mode 100644 index 000000000..c8c949a42 --- /dev/null +++ b/test/e2e/tests/lock-account.spec.js @@ -0,0 +1,42 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Lock and unlock', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('successfully unlocks after lock', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + const lockButton = await driver.findClickableElement( + '.account-menu__lock-button', + ); + assert.equal(await lockButton.getText(), 'Lock'); + await lockButton.click(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + const walletBalance = await driver.findElement( + '[data-testid="wallet-balance"] .list-item__heading', + ); + assert.equal(/^25\s*ETH$/u.test(await walletBalance.getText()), true); + }, + ); + }); +}); diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index 2c1c1c97e..bd66612ba 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -217,3 +217,120 @@ describe('Send ETH from inside MetaMask using advanced gas modal', function () { ); }); }); + +describe('Send ETH from dapp using advanced gas controls', function () { + let windowHandles; + let extension; + let popup; + let dapp; + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('should display the correct gas price on the transaction', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // goes to the settings screen + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ text: 'Advanced', tag: 'div' }); + await driver.clickElement( + '[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > div > div', + ); + const advancedGasTitle = await driver.findElement({ + text: 'Advanced gas controls', + tag: 'span', + }); + await driver.scrollToElement(advancedGasTitle); + await driver.clickElement( + '[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > div > div', + ); + windowHandles = await driver.getAllWindowHandles(); + extension = windowHandles[0]; + await driver.closeAllWindowHandlesExcept([extension]); + await driver.clickElement('.app-header__logo-container'); + + // connects the dapp + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); + extension = windowHandles[0]; + dapp = await driver.switchToWindowWithTitle( + 'E2E Test Dapp', + windowHandles, + ); + popup = windowHandles.find( + (handle) => handle !== extension && handle !== dapp, + ); + await driver.switchToWindow(popup); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(dapp); + + // initiates a send from the dapp + await driver.clickElement({ text: 'Send', tag: 'button' }, 10000); + await driver.delay(2000); + windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + await driver.assertElementNotPresent({ text: 'Data', tag: 'li' }); + const [gasPriceInput, gasLimitInput] = await driver.findElements( + '.advanced-gas-inputs__gas-edit-row__input', + ); + await gasPriceInput.clear(); + await driver.delay(50); + await gasPriceInput.fill('10'); + await driver.delay(50); + await driver.delay(50); + await gasLimitInput.fill(''); + await driver.delay(50); + await gasLimitInput.fill('25000'); + await driver.delay(1000); + await driver.clickElement({ text: 'Confirm', tag: 'button' }, 10000); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + + // finds the transaction in the transactions list + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', + { timeout: 10000 }, + ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-3 ETH', + }); + + // the transaction has the expected gas price + const txValue = await driver.findClickableElement( + '.transaction-list-item__primary-currency', + ); + await txValue.click(); + const gasPrice = await driver.waitForSelector({ + css: '[data-testid="transaction-breakdown__gas-price"]', + text: '10', + }); + assert.equal(await gasPrice.getText(), '10'); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/chrome.js b/test/e2e/webdriver/chrome.js index e0bb65626..bf3c2d023 100644 --- a/test/e2e/webdriver/chrome.js +++ b/test/e2e/webdriver/chrome.js @@ -14,10 +14,21 @@ class ChromeDriver { const builder = new Builder() .forBrowser('chrome') .setChromeOptions(options); + const service = new chrome.ServiceBuilder(); + + // Enables Chrome logging. + // Especially useful for discovering why Chrome has crashed, but can also + // be useful for revealing console errors (from the page or background). + if ( + process.env.ENABLE_CHROME_LOGGING && + process.env.ENABLE_CHROME_LOGGING !== 'false' + ) { + service.setStdio('inherit').enableChromeLogging(); + } if (port) { - const service = new chrome.ServiceBuilder().setPort(port); - builder.setChromeService(service); + service.setPort(port); } + builder.setChromeService(service); const driver = builder.build(); const chromeDriver = new ChromeDriver(driver); const extensionId = await chromeDriver.getExtensionIdByName('MetaMask'); diff --git a/test/e2e/webdriver/firefox.js b/test/e2e/webdriver/firefox.js index 6d327305a..d9d105b98 100644 --- a/test/e2e/webdriver/firefox.js +++ b/test/e2e/webdriver/firefox.js @@ -75,9 +75,7 @@ class FirefoxDriver { await this._driver.get('about:debugging#addons'); return await this._driver .wait( - until.elementLocated( - By.xpath("//dl/div[contains(., 'Internal UUID')]/dd"), - ), + until.elementLocated(By.xpath("//dl/div[contains(., 'UUID')]/dd")), 1000, ) .getText(); diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 7a8057ecc..325c9f2b5 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -4,7 +4,19 @@ export const createSwapsMockStore = () => { return { swaps: { customGas: { + limit: '0x0', fallBackPrice: 5, + priceEstimates: { + blockTime: 14.1, + safeLow: 2.5, + safeLowWait: 6.6, + average: 4, + avgWait: 5.3, + fast: 5, + fastWait: 3.3, + fastest: 10, + fastestWait: 0.5, + }, }, fromToken: 'ETH', }, @@ -84,7 +96,108 @@ export const createSwapsMockStore = () => { }, ], swapsState: { - quotes: {}, + quotes: { + TEST_AGG_1: { + trade: { + from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + value: '0x0', + gas: '0x61a80', // 4e5 + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + }, + sourceAmount: '10000000000000000000', // 10e18 + destinationAmount: '20000000000000000000', // 20e18 + error: null, + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + approvalNeeded: null, + maxGas: 600000, + averageGas: 120000, + estimatedRefund: 80000, + fetchTime: 607, + aggregator: 'TEST_AGG_1', + aggType: 'AGG', + slippage: 2, + sourceTokenInfo: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + decimals: 18, + iconUrl: 'https://foo.bar/logo.png', + }, + destinationTokenInfo: { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + decimals: 18, + }, + fee: 1, + }, + + TEST_AGG_BEST: { + trade: { + from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + value: '0x0', + gas: '0x61a80', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + }, + sourceAmount: '10000000000000000000', + destinationAmount: '25000000000000000000', // 25e18 + error: null, + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + approvalNeeded: null, + maxGas: 1100000, + averageGas: 411000, + estimatedRefund: 343090, + fetchTime: 1003, + aggregator: 'TEST_AGG_BEST', + aggType: 'AGG', + slippage: 2, + sourceTokenInfo: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + decimals: 18, + iconUrl: 'https://foo.bar/logo.png', + }, + destinationTokenInfo: { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + decimals: 18, + }, + fee: 1, + }, + TEST_AGG_2: { + trade: { + from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + value: '0x0', + gas: '0x61a80', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + }, + sourceAmount: '10000000000000000000', + destinationAmount: '22000000000000000000', // 22e18 + error: null, + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + approvalNeeded: null, + maxGas: 368000, + averageGas: 197000, + estimatedRefund: 18205, + fetchTime: 1354, + aggregator: 'TEST_AGG_2', + aggType: 'AGG', + slippage: 2, + sourceTokenInfo: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + decimals: 18, + iconUrl: 'https://foo.bar/logo.png', + }, + destinationTokenInfo: { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + decimals: 18, + }, + fee: 1, + }, + }, fetchParams: { metaData: { sourceTokenInfo: { @@ -97,10 +210,11 @@ export const createSwapsMockStore = () => { }, tradeTxId: null, approveTxId: null, - quotesLastFetched: null, + quotesLastFetched: 1519211809934, + swapsQuoteRefreshTime: 60000, customMaxGas: '', customGasPrice: null, - selectedAggId: null, + selectedAggId: 'TEST_AGG_2', customApproveTxData: '', errorKey: '', topAggId: null, @@ -109,5 +223,17 @@ export const createSwapsMockStore = () => { useNewSwapsApi: false, }, }, + appState: { + modal: { + open: true, + modalState: { + name: 'test', + props: { + initialGasLimit: 100, + minimumGasLimit: 5, + }, + }, + }, + }, }; }; diff --git a/test/unit-global/frozenPromise.test.js b/test/unit-global/frozenPromise.test.js index f96af0d2a..b41362266 100644 --- a/test/unit-global/frozenPromise.test.js +++ b/test/unit-global/frozenPromise.test.js @@ -1,7 +1,7 @@ // Should occur before anything else import './globalPatch'; import 'ses/lockdown'; -import '../../app/scripts/runLockdown'; +import '../../app/scripts/lockdown-run'; import { strict as assert } from 'assert'; /* eslint-disable-line import/first,import/order */ describe('Promise global is immutable', function () { diff --git a/ui/__mocks__/react-router-dom.js b/ui/__mocks__/react-router-dom.js index dd15434a1..1f65e3395 100644 --- a/ui/__mocks__/react-router-dom.js +++ b/ui/__mocks__/react-router-dom.js @@ -2,7 +2,9 @@ const originalModule = jest.requireActual('react-router-dom'); module.exports = { ...originalModule, - useHistory: jest.fn(), + useHistory: jest.fn(() => { + return []; + }), useLocation: jest.fn(() => { return { pathname: '/swaps/build-quote', diff --git a/ui/components/app/account-menu/account-menu.component.js b/ui/components/app/account-menu/account-menu.component.js index f6c5b75df..aec8decfb 100644 --- a/ui/components/app/account-menu/account-menu.component.js +++ b/ui/components/app/account-menu/account-menu.component.js @@ -253,12 +253,13 @@ export default class AccountMenu extends Component { } setShouldShowScrollButton = () => { - const { scrollTop, offsetHeight, scrollHeight } = this.accountsRef; + if (!this.accountsRef) { + return; + } + const { scrollTop, offsetHeight, scrollHeight } = this.accountsRef; const canScroll = scrollHeight > offsetHeight; - const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight; - const shouldShowScrollButton = canScroll && !atAccountListBottom; this.setState({ shouldShowScrollButton }); diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js new file mode 100644 index 000000000..ba0679dee --- /dev/null +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js @@ -0,0 +1,233 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { I18nContext } from '../../../contexts/i18n'; +import Typography from '../../ui/typography/typography'; +import { + FONT_WEIGHT, + TYPOGRAPHY, + COLORS, +} from '../../../helpers/constants/design-system'; +import FormField from '../../ui/form-field'; +import { + GAS_ESTIMATE_TYPES, + GAS_RECOMMENDATIONS, +} from '../../../../shared/constants/gas'; +import { getGasFormErrorText } from '../../../helpers/constants/gas'; + +const DEFAULT_ESTIMATES_LEVEL = 'medium'; + +export default function AdvancedGasControls({ + estimateToUse, + gasFeeEstimates, + gasEstimateType, + maxPriorityFee, + maxFee, + setMaxPriorityFee, + setMaxFee, + onManualChange, + gasLimit, + setGasLimit, + gasPrice, + setGasPrice, + maxPriorityFeeFiat, + maxFeeFiat, + gasErrors, +}) { + const t = useContext(I18nContext); + + const suggestedValues = {}; + + switch (gasEstimateType) { + case GAS_ESTIMATE_TYPES.FEE_MARKET: + suggestedValues.maxPriorityFeePerGas = + gasFeeEstimates?.[estimateToUse]?.suggestedMaxPriorityFeePerGas; + suggestedValues.maxFeePerGas = + gasFeeEstimates?.[estimateToUse]?.suggestedMaxFeePerGas; + break; + case GAS_ESTIMATE_TYPES.LEGACY: + suggestedValues.gasPrice = gasFeeEstimates?.[estimateToUse]; + break; + case GAS_ESTIMATE_TYPES.ETH_GASPRICE: + suggestedValues.gasPrice = gasFeeEstimates?.gasPrice; + break; + default: + break; + } + + const showFeeMarketFields = + process.env.SHOW_EIP_1559_UI && + gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET; + + return ( +
    + + {showFeeMarketFields ? ( + <> + { + onManualChange?.(); + setMaxPriorityFee(value); + }} + value={maxPriorityFee} + detailText={maxPriorityFeeFiat} + numeric + titleDetail={ + suggestedValues.maxPriorityFeePerGas && ( + <> + + {t('gasFeeEstimate')}: + {' '} + + { + gasFeeEstimates?.[DEFAULT_ESTIMATES_LEVEL] + ?.suggestedMaxPriorityFeePerGas + } + + + ) + } + error={ + gasErrors?.maxPriorityFee + ? getGasFormErrorText(gasErrors.maxPriorityFee, t) + : null + } + /> + { + onManualChange?.(); + setMaxFee(value); + }} + value={maxFee} + numeric + detailText={maxFeeFiat} + titleDetail={ + suggestedValues.maxFeePerGas && ( + <> + + {t('gasFeeEstimate')}: + {' '} + + { + gasFeeEstimates?.[DEFAULT_ESTIMATES_LEVEL] + ?.suggestedMaxFeePerGas + } + + + ) + } + error={ + gasErrors?.maxFee + ? getGasFormErrorText(gasErrors.maxFee, t) + : null + } + /> + + ) : ( + <> + { + onManualChange?.(); + setGasPrice(value); + }} + tooltipText={t('editGasPriceTooltip')} + value={gasPrice} + numeric + titleDetail={ + suggestedValues.gasPrice && ( + <> + + {t('gasFeeEstimate')}: + {' '} + + {suggestedValues.gasPrice} + + + ) + } + /> + + )} +
    + ); +} + +AdvancedGasControls.propTypes = { + estimateToUse: PropTypes.oneOf(Object.values(GAS_RECOMMENDATIONS)), + gasFeeEstimates: PropTypes.oneOf([ + PropTypes.shape({ + gasPrice: PropTypes.string, + }), + PropTypes.shape({ + low: PropTypes.string, + medium: PropTypes.string, + high: PropTypes.string, + }), + PropTypes.shape({ + low: PropTypes.object, + medium: PropTypes.object, + high: PropTypes.object, + estimatedBaseFee: PropTypes.string, + }), + ]), + gasEstimateType: PropTypes.oneOf(Object.values(GAS_ESTIMATE_TYPES)), + setMaxPriorityFee: PropTypes.func, + setMaxFee: PropTypes.func, + maxPriorityFee: PropTypes.number, + maxFee: PropTypes.number, + onManualChange: PropTypes.func, + gasLimit: PropTypes.number, + setGasLimit: PropTypes.func, + gasPrice: PropTypes.number, + setGasPrice: PropTypes.func, + maxPriorityFeeFiat: PropTypes.string, + maxFeeFiat: PropTypes.string, + gasErrors: PropTypes.object, +}; diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js new file mode 100644 index 000000000..150380b1a --- /dev/null +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import AdvancedGasControls from '.'; + +export default { + title: 'Advanced Gas Controls', +}; + +export const simple = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/app/advanced-gas-controls/index.js b/ui/components/app/advanced-gas-controls/index.js new file mode 100644 index 000000000..ceedba1ec --- /dev/null +++ b/ui/components/app/advanced-gas-controls/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-controls.component'; diff --git a/ui/components/app/advanced-gas-controls/index.scss b/ui/components/app/advanced-gas-controls/index.scss new file mode 100644 index 000000000..7fdf9174f --- /dev/null +++ b/ui/components/app/advanced-gas-controls/index.scss @@ -0,0 +1,35 @@ +.advanced-gas-controls { + &__row { + margin-bottom: 20px; + } + + &__row-heading { + display: flex; + } + + .info-tooltip { + display: inline-block; + } + + &__row-heading-detail { + flex-grow: 1; + align-self: center; + } + + .form-field__row--error .form-field__heading-title h6 { + color: $error-1; + + & path { + fill: $error-1; + } + } + + h6 { + padding-bottom: 6px; + margin-inline-end: 6px; + } + + path { + fill: #dadada; + } +} diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 89f2b9760..c868491ac 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -2,6 +2,7 @@ @import 'account-list-item/index'; @import 'account-menu/index'; @import 'add-token-button/index'; +@import 'advanced-gas-controls/index'; @import 'alerts/alerts'; @import 'app-header/index'; @import 'asset-list-item/asset-list-item'; @@ -10,9 +11,12 @@ @import 'connected-accounts-permissions/index'; @import 'connected-sites-list/index'; @import 'connected-status-indicator/index'; +@import 'edit-gas-display/index'; +@import 'edit-gas-display-education/index'; @import 'gas-customization/gas-modal-page-container/index'; @import 'gas-customization/gas-price-button-group/index'; @import 'gas-customization/index'; +@import 'gas-timing/index'; @import 'home-notification/index'; @import 'info-box/index'; @import 'menu-bar/index'; @@ -24,6 +28,7 @@ @import 'permissions-connect-footer/index'; @import 'permissions-connect-header/index'; @import 'recovery-phrase-reminder/index'; +@import 'step-progress-bar/index.scss'; @import 'selected-account/index'; @import 'sidebars/index'; @import 'signature-request/index'; @@ -32,10 +37,13 @@ @import 'token-cell/token-cell'; @import 'transaction-activity-log/index'; @import 'transaction-breakdown/index'; +@import 'transaction-detail/index'; +@import 'transaction-detail-item/index'; @import 'transaction-icon/transaction-icon'; @import 'transaction-list-item-details/index'; @import 'transaction-list-item/index'; @import 'transaction-list/index'; @import 'transaction-status/index'; +@import 'transaction-total-banner/index'; @import 'wallet-overview/index'; @import 'whats-new-popup/index'; diff --git a/ui/components/app/asset-list-item/asset-list-item.scss b/ui/components/app/asset-list-item/asset-list-item.scss index 6ee0853a5..f46fe7928 100644 --- a/ui/components/app/asset-list-item/asset-list-item.scss +++ b/ui/components/app/asset-list-item/asset-list-item.scss @@ -10,10 +10,7 @@ text-align: start; & h2 { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 100%; + display: flex; } & span { @@ -21,6 +18,16 @@ } } + &__token-value { + flex: 1; + padding-right: 5px; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 100%; + display: block; + overflow: hidden; + } + &__chevron-right { color: $Grey-500; } diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index e47bfaf78..28d5f578e 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -7,6 +7,10 @@ import { PageContainerFooter } from '../../../ui/page-container'; import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.'; export default class ConfirmPageContainerContent extends Component { + static contextTypes = { + t: PropTypes.func.isRequired, + }; + static propTypes = { action: PropTypes.string, dataComponent: PropTypes.node, @@ -44,14 +48,18 @@ export default class ConfirmPageContainerContent extends Component { } renderTabs() { + const { t } = this.context; const { detailsComponent, dataComponent } = this.props; return ( - + {detailsComponent} - + {dataComponent} diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 71923e546..9cc2e4286 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -2,6 +2,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import SenderToRecipient from '../../ui/sender-to-recipient'; import { PageContainerFooter } from '../../ui/page-container'; +import EditGasPopover from '../edit-gas-popover'; +import { EDIT_GAS_MODES } from '../../../../shared/constants/gas'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -60,6 +62,10 @@ export default class ConfirmPageContainer extends Component { onCancel: PropTypes.func, onSubmit: PropTypes.func, disabled: PropTypes.bool, + editingGas: PropTypes.bool, + handleCloseEditGas: PropTypes.func, + // Gas Popover + currentTransaction: PropTypes.object.isRequired, }; render() { @@ -105,6 +111,9 @@ export default class ConfirmPageContainer extends Component { showAccountInHeader, origin, ethGasPriceWarning, + editingGas, + handleCloseEditGas, + currentTransaction, } = this.props; const renderAssetImage = contentComponent || !identiconAddress; @@ -183,6 +192,13 @@ export default class ConfirmPageContainer extends Component { )} )} + {editingGas && ( + + )} ); } diff --git a/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js b/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js new file mode 100644 index 000000000..ab2fd2104 --- /dev/null +++ b/ui/components/app/edit-gas-display-education/edit-gas-display-education.component.js @@ -0,0 +1,62 @@ +import React, { useContext } from 'react'; + +import Typography from '../../ui/typography/typography'; +import { + COLORS, + TYPOGRAPHY, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; + +import { I18nContext } from '../../../contexts/i18n'; + +export default function EditGasDisplayEducation() { + const t = useContext(I18nContext); + + return ( +
    + + {t('editGasEducationModalIntro')} + + + {t('editGasHigh')} + + + {t('editGasEducationHighExplanation')} + + + {t('editGasMedium')} + + + {t('editGasEducationMediumExplanation')} + + + {t('editGasLow')} + + + {t('editGasEducationLowExplanation')} + + + + {t('editGasEducationLearnMoreLinkText')} + + +
    + ); +} diff --git a/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js b/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js new file mode 100644 index 000000000..aed0fa9c8 --- /dev/null +++ b/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import EditGasDisplayEducation from '.'; + +export default { + title: 'Edit Gas Display', +}; + +export const basic = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/app/edit-gas-display-education/index.js b/ui/components/app/edit-gas-display-education/index.js new file mode 100644 index 000000000..dec57af8b --- /dev/null +++ b/ui/components/app/edit-gas-display-education/index.js @@ -0,0 +1 @@ +export { default } from './edit-gas-display-education.component'; diff --git a/ui/components/app/edit-gas-display-education/index.scss b/ui/components/app/edit-gas-display-education/index.scss new file mode 100644 index 000000000..dc8d39fd8 --- /dev/null +++ b/ui/components/app/edit-gas-display-education/index.scss @@ -0,0 +1,5 @@ +.edit-gas-display-education { + a { + color: $primary-1; + } +} diff --git a/ui/components/app/edit-gas-display/edit-gas-display.component.js b/ui/components/app/edit-gas-display/edit-gas-display.component.js new file mode 100644 index 000000000..a47e87c2d --- /dev/null +++ b/ui/components/app/edit-gas-display/edit-gas-display.component.js @@ -0,0 +1,252 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { + GAS_RECOMMENDATIONS, + EDIT_GAS_MODES, +} from '../../../../shared/constants/gas'; + +import Button from '../../ui/button'; +import Typography from '../../ui/typography/typography'; +import { + COLORS, + TYPOGRAPHY, + FONT_WEIGHT, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; + +import InfoTooltip from '../../ui/info-tooltip'; +import TransactionTotalBanner from '../transaction-total-banner/transaction-total-banner.component'; +import RadioGroup from '../../ui/radio-group/radio-group.component'; +import AdvancedGasControls from '../advanced-gas-controls/advanced-gas-controls.component'; +import ActionableMessage from '../../ui/actionable-message/actionable-message'; + +import { I18nContext } from '../../../contexts/i18n'; +import GasTiming from '../gas-timing'; + +export default function EditGasDisplay({ + mode = EDIT_GAS_MODES.MODIFY_IN_PLACE, + showEducationButton = false, + onEducationClick, + transaction, + defaultEstimateToUse, + maxPriorityFeePerGas, + setMaxPriorityFeePerGas, + maxPriorityFeePerGasFiat, + maxFeePerGas, + setMaxFeePerGas, + maxFeePerGasFiat, + estimatedMaximumNative, + isGasEstimatesLoading, + gasFeeEstimates, + gasEstimateType, + gasPrice, + setGasPrice, + gasLimit, + setGasLimit, + estimateToUse, + setEstimateToUse, + estimatedMinimumFiat, + estimatedMaximumFiat, + hasGasErrors, + dappSuggestedGasFeeAcknowledged, + setDappSuggestedGasFeeAcknowledged, + showAdvancedForm, + setShowAdvancedForm, + warning, + gasErrors, + onManualChange, +}) { + const t = useContext(I18nContext); + + const alwaysShowForm = !estimateToUse || hasGasErrors || false; + + const requireDappAcknowledgement = Boolean( + transaction?.dappSuggestedGasFees && !dappSuggestedGasFeeAcknowledged, + ); + + return ( +
    +
    + {warning && ( +
    + +
    + )} + {requireDappAcknowledgement && ( +
    + +
    + )} + {mode === EDIT_GAS_MODES.SPEED_UP && ( +
    + + {t('speedUpTooltipText')}{' '} + + +
    + )} + + {estimatedMaximumFiat} + , + + {estimatedMaximumNative} + , + ]) + } + timing={} + /> + {requireDappAcknowledgement && ( + + )} + {hasGasErrors && ( +
    + + {t('editGasTooLow')}{' '} + + +
    + )} + {!requireDappAcknowledgement && + ![EDIT_GAS_MODES.SPEED_UP, EDIT_GAS_MODES.CANCEL].includes(mode) && ( + + )} + {!alwaysShowForm && !requireDappAcknowledgement && ( + + )} + {!requireDappAcknowledgement && + (alwaysShowForm || showAdvancedForm) && ( + + )} +
    + {!requireDappAcknowledgement && showEducationButton && ( +
    + +
    + )} +
    + ); +} + +EditGasDisplay.propTypes = { + mode: PropTypes.oneOf(Object.values(EDIT_GAS_MODES)), + showEducationButton: PropTypes.bool, + onEducationClick: PropTypes.func, + defaultEstimateToUse: PropTypes.oneOf(Object.values(GAS_RECOMMENDATIONS)), + maxPriorityFeePerGas: PropTypes.string, + setMaxPriorityFeePerGas: PropTypes.func, + maxPriorityFeePerGasFiat: PropTypes.string, + maxFeePerGas: PropTypes.string, + setMaxFeePerGas: PropTypes.func, + maxFeePerGasFiat: PropTypes.string, + estimatedMaximumNative: PropTypes.string, + isGasEstimatesLoading: PropTypes.boolean, + gasFeeEstimates: PropTypes.object, + gasEstimateType: PropTypes.string, + gasPrice: PropTypes.string, + setGasPrice: PropTypes.func, + gasLimit: PropTypes.number, + setGasLimit: PropTypes.func, + estimateToUse: PropTypes.string, + setEstimateToUse: PropTypes.func, + estimatedMinimumFiat: PropTypes.string, + estimatedMaximumFiat: PropTypes.string, + hasGasErrors: PropTypes.boolean, + dappSuggestedGasFeeAcknowledged: PropTypes.boolean, + setDappSuggestedGasFeeAcknowledged: PropTypes.func, + showAdvancedForm: PropTypes.bool, + setShowAdvancedForm: PropTypes.func, + warning: PropTypes.string, + transaction: PropTypes.object, + gasErrors: PropTypes.object, + onManualChange: PropTypes.func, +}; diff --git a/ui/components/app/edit-gas-display/edit-gas-display.stories.js b/ui/components/app/edit-gas-display/edit-gas-display.stories.js new file mode 100644 index 000000000..6dd89f931 --- /dev/null +++ b/ui/components/app/edit-gas-display/edit-gas-display.stories.js @@ -0,0 +1,33 @@ +import React from 'react'; +import EditGasDisplay from '.'; + +export default { + title: 'Edit Gas Display', +}; + +export const basic = () => { + return ( +
    + +
    + ); +}; + +export const withEducation = () => { + return ( +
    + +
    + ); +}; + +export const withDappSuggestedGas = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/app/edit-gas-display/index.js b/ui/components/app/edit-gas-display/index.js new file mode 100644 index 000000000..d2f752891 --- /dev/null +++ b/ui/components/app/edit-gas-display/index.js @@ -0,0 +1 @@ +export { default } from './edit-gas-display.component'; diff --git a/ui/components/app/edit-gas-display/index.scss b/ui/components/app/edit-gas-display/index.scss new file mode 100644 index 000000000..e3ccea500 --- /dev/null +++ b/ui/components/app/edit-gas-display/index.scss @@ -0,0 +1,72 @@ +.edit-gas-display { + & .actionable-message--warning, + & .actionable-message--error { + margin-top: 0; + + & .actionable-message__message { + text-align: start; + } + } + + &__top-tooltip { + text-align: center; + + .info-tooltip { + display: inline-block; + + img { + height: 10px; + width: 10px; + } + } + } + + &__error .info-tooltip { + display: inline-block; + + path { + fill: $error-1; + } + } + + &__dapp-acknowledgement-warning { + margin-bottom: 20px; + } + + button.edit-gas-display__dapp-acknowledgement-button { + margin: 40px auto 0 auto; + display: block; + color: $secondary-1; + border: 1px solid $secondary-1; + text-transform: unset; + width: auto; + background: transparent; + } + + .radio-group { + margin: 20px auto; + } + + &__advanced-button { + display: block; + margin: 0 auto; + background: transparent; + color: $primary-1; + font-weight: bold; + } + + .advanced-gas-controls { + margin-top: 20px; + } + + &__education { + margin-top: 20px; + + button { + display: block; + margin: 0 auto; + background: transparent; + color: $primary-1; + } + } +} diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js new file mode 100644 index 000000000..7a862e42d --- /dev/null +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js @@ -0,0 +1,244 @@ +import React, { useCallback, useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { useDispatch, useSelector } from 'react-redux'; +import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs'; +import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; + +import { + GAS_ESTIMATE_TYPES, + EDIT_GAS_MODES, +} from '../../../../shared/constants/gas'; + +import { + decGWEIToHexWEI, + decimalToHex, +} from '../../../helpers/utils/conversions.util'; + +import Popover from '../../ui/popover'; +import Button from '../../ui/button'; +import EditGasDisplay from '../edit-gas-display'; +import EditGasDisplayEducation from '../edit-gas-display-education'; + +import { I18nContext } from '../../../contexts/i18n'; +import { + createCancelTransaction, + createSpeedUpTransaction, + hideModal, + hideSidebar, + updateTransaction, +} from '../../../store/actions'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; + +export default function EditGasPopover({ + popoverTitle = '', + confirmButtonText = '', + editGasDisplayProps = {}, + defaultEstimateToUse = 'medium', + transaction, + mode, + onClose, +}) { + const t = useContext(I18nContext); + const dispatch = useDispatch(); + const showSidebar = useSelector((state) => state.appState.sidebar.isOpen); + + const shouldAnimate = useShouldAnimateGasEstimations(); + + const showEducationButton = + mode === EDIT_GAS_MODES.MODIFY_IN_PLACE && process.env.SHOW_EIP_1559_UI; + const [showEducationContent, setShowEducationContent] = useState(false); + + const [warning] = useState(null); + + const [showAdvancedForm, setShowAdvancedForm] = useState(false); + const [ + dappSuggestedGasFeeAcknowledged, + setDappSuggestedGasFeeAcknowledged, + ] = useState(false); + + const { + maxPriorityFeePerGas, + setMaxPriorityFeePerGas, + maxPriorityFeePerGasFiat, + maxFeePerGas, + setMaxFeePerGas, + maxFeePerGasFiat, + estimatedMaximumNative, + isGasEstimatesLoading, + gasFeeEstimates, + gasEstimateType, + gasPrice, + setGasPrice, + gasLimit, + setGasLimit, + estimateToUse, + setEstimateToUse, + estimatedMinimumFiat, + estimatedMaximumFiat, + hasGasErrors, + gasErrors, + onManualChange, + } = useGasFeeInputs(defaultEstimateToUse); + + /** + * Temporary placeholder, this should be managed by the parent component but + * we will be extracting this component from the hard to maintain modal/ + * sidebar component. For now this is just to be able to appropriately close + * the modal in testing + */ + const closePopover = useCallback(() => { + if (onClose) { + onClose(); + } else if (showSidebar) { + dispatch(hideSidebar()); + } else { + dispatch(hideModal()); + } + }, [showSidebar, onClose, dispatch]); + + const onSubmit = useCallback(() => { + if (!transaction || !mode) { + closePopover(); + } + + const newGasSettings = + gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET + ? { + gas: decimalToHex(gasLimit), + gasLimit: decimalToHex(gasLimit), + maxFeePerGas: decGWEIToHexWEI(maxFeePerGas), + maxPriorityFeePerGas: decGWEIToHexWEI(maxPriorityFeePerGas), + } + : { + gas: decimalToHex(gasLimit), + gasLimit: decimalToHex(gasLimit), + gasPrice: decGWEIToHexWEI(gasPrice), + }; + + switch (mode) { + case EDIT_GAS_MODES.CANCEL: + dispatch(createCancelTransaction(transaction.id, newGasSettings)); + break; + case EDIT_GAS_MODES.SPEED_UP: + dispatch(createSpeedUpTransaction(transaction.id, newGasSettings)); + break; + case EDIT_GAS_MODES.MODIFY_IN_PLACE: + dispatch( + updateTransaction({ + ...transaction, + txParams: { + ...transaction.txParams, + ...newGasSettings, + }, + }), + ); + break; + default: + break; + } + + closePopover(); + }, [ + transaction, + mode, + dispatch, + closePopover, + gasLimit, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + gasEstimateType, + ]); + + let title = t('editGasTitle'); + if (popoverTitle) { + title = popoverTitle; + } else if (showEducationContent) { + title = t('editGasEducationModalTitle'); + } else if (mode === EDIT_GAS_MODES.SPEED_UP) { + title = t('speedUpPopoverTitle'); + } else if (mode === EDIT_GAS_MODES.CANCEL) { + title = t('cancelPopoverTitle'); + } + + const footerButtonText = confirmButtonText || t('save'); + + return ( + setShowEducationContent(false) : undefined + } + footer={ + showEducationContent ? null : ( + <> + + + ) + } + > +
    + {showEducationContent ? ( + + ) : ( + <> + + setShowEducationContent(true)} + mode={mode} + transaction={transaction} + hasGasErrors={hasGasErrors} + gasErrors={gasErrors} + onManualChange={onManualChange} + {...editGasDisplayProps} + /> + + )} +
    +
    + ); +} + +EditGasPopover.propTypes = { + popoverTitle: PropTypes.string, + editGasDisplayProps: PropTypes.object, + confirmButtonText: PropTypes.string, + onClose: PropTypes.func, + transaction: PropTypes.object, + mode: PropTypes.oneOf(Object.values(EDIT_GAS_MODES)), + defaultEstimateToUse: PropTypes.string, +}; diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js b/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js new file mode 100644 index 000000000..76c7dafae --- /dev/null +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js @@ -0,0 +1,30 @@ +import React from 'react'; +import EditGasPopover from '.'; + +export default { + title: 'Edit Gas Display Popover', +}; + +export const basic = () => { + return ( +
    + +
    + ); +}; + +export const basicWithDifferentButtonText = () => { + return ( +
    + +
    + ); +}; + +export const educationalContentFlow = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/app/edit-gas-popover/index.js b/ui/components/app/edit-gas-popover/index.js new file mode 100644 index 000000000..98a908824 --- /dev/null +++ b/ui/components/app/edit-gas-popover/index.js @@ -0,0 +1 @@ +export { default } from './edit-gas-popover.component'; diff --git a/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index a775cec65..21a76c41a 100644 --- a/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { debounce } from 'lodash'; import Tooltip from '../../../ui/tooltip'; -import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; export default class AdvancedGasInputs extends Component { static contextTypes = { @@ -24,7 +23,6 @@ export default class AdvancedGasInputs extends Component { }; static defaultProps = { - minimumGasLimit: Number(MIN_GAS_LIMIT_DEC), customPriceIsExcessive: false, }; diff --git a/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js index 45cdaaa38..09eb26fdd 100644 --- a/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js +++ b/ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -4,6 +4,7 @@ import { decimalToHex, hexWEIToDecGWEI, } from '../../../../helpers/utils/conversions.util'; +import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; import AdvancedGasInputs from './advanced-gas-inputs.component'; function convertGasPriceForInputs(gasPriceInHexWEI) { @@ -14,12 +15,17 @@ function convertGasLimitForInputs(gasLimitInHexWEI) { return parseInt(gasLimitInHexWEI, 16) || 0; } +function convertMinimumGasLimitForInputs(minimumGasLimit = MIN_GAS_LIMIT_DEC) { + return parseInt(minimumGasLimit, 10); +} + const mergeProps = (stateProps, dispatchProps, ownProps) => { const { customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit, + minimumGasLimit, } = ownProps; return { ...ownProps, @@ -27,6 +33,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...dispatchProps, customGasPrice: convertGasPriceForInputs(customGasPrice), customGasLimit: convertGasLimitForInputs(customGasLimit), + minimumGasLimit: convertMinimumGasLimitForInputs(minimumGasLimit), updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)), updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)), diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js index d88a3ab98..5b8f5c665 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js @@ -1,22 +1,23 @@ import React from 'react'; import sinon from 'sinon'; import { shallowWithContext } from '../../../../../test/lib/render-helpers'; +import { getGasFeeEstimatesAndStartPolling } from '../../../../store/actions'; import PageContainer from '../../../ui/page-container'; import { Tab } from '../../../ui/tabs'; import GasModalPageContainer from './gas-modal-page-container.component'; -const mockBasicGasEstimates = { - average: '20', -}; +jest.mock('../../../../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), +})); const propsMethodSpies = { cancelAndClose: sinon.spy(), onSubmit: sinon.spy(), - fetchBasicGasEstimates: sinon - .stub() - .returns(Promise.resolve(mockBasicGasEstimates)), }; const mockGasPriceButtonGroupProps = { @@ -67,7 +68,6 @@ describe('GasModalPageContainer Component', () => { 'mockupdateCustomGasPrice'} updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} @@ -83,18 +83,15 @@ describe('GasModalPageContainer Component', () => { afterEach(() => { propsMethodSpies.cancelAndClose.resetHistory(); + jest.clearAllMocks(); }); describe('componentDidMount', () => { - it('should call props.fetchBasicGasEstimates', () => { - propsMethodSpies.fetchBasicGasEstimates.resetHistory(); - expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual( - 0, - ); + it('should call getGasFeeEstimatesAndStartPolling', () => { + jest.clearAllMocks(); + expect(getGasFeeEstimatesAndStartPolling).not.toHaveBeenCalled(); wrapper.instance().componentDidMount(); - expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual( - 1, - ); + expect(getGasFeeEstimatesAndStartPolling).toHaveBeenCalled(); }); }); @@ -120,20 +117,18 @@ describe('GasModalPageContainer Component', () => { }); it('should pass the correct renderTabs property to PageContainer', () => { - sinon.stub(GP, 'renderTabs').returns('mockTabs'); + jest + .spyOn(GasModalPageContainer.prototype, 'renderTabs') + .mockImplementation(() => 'mockTabs'); const renderTabsWrapperTester = shallowWithContext( - , + , { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); const { tabsComponent } = renderTabsWrapperTester .find(PageContainer) .props(); expect(tabsComponent).toStrictEqual('mockTabs'); - GasModalPageContainer.prototype.renderTabs.restore(); + GasModalPageContainer.prototype.renderTabs.mockClear(); }); }); @@ -195,7 +190,6 @@ describe('GasModalPageContainer Component', () => { 'mockupdateCustomGasPrice'} updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js index d82a8fb0a..37e853cd1 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js @@ -49,6 +49,10 @@ jest.mock('../../../../store/actions', () => ({ updateTransaction: jest.fn(), })); +jest.mock('../../../../ducks/metamask/metamask.js', () => ({ + updateTransactionGasFees: jest.fn(), +})); + jest.mock('../../../../ducks/gas/gas.duck', () => ({ setCustomGasPrice: jest.fn(), setCustomGasLimit: jest.fn(), @@ -79,6 +83,7 @@ describe('gas-modal-page-container container', () => { afterEach(() => { dispatchSpy.resetHistory(); + jest.clearAllMocks(); }); describe('useCustomGas()', () => { @@ -137,17 +142,6 @@ describe('gas-modal-page-container container', () => { expect(updateGasPrice).toHaveBeenCalledWith('aaaa'); }); }); - - describe('updateConfirmTxGasAndCalculate()', () => { - it('should dispatch a updateGasAndCalculate action with the correct props', () => { - mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa'); - expect(dispatchSpy.callCount).toStrictEqual(3); - expect(setCustomGasPrice).toHaveBeenCalled(); - expect(setCustomGasLimit).toHaveBeenCalled(); - expect(setCustomGasLimit).toHaveBeenCalledWith('0xffff'); - expect(setCustomGasPrice).toHaveBeenCalledWith('0xaaaa'); - }); - }); }); describe('mergeProps', () => { @@ -169,7 +163,7 @@ describe('gas-modal-page-container container', () => { updateCustomGasPrice: sinon.spy(), useCustomGas: sinon.spy(), setGasData: sinon.spy(), - updateConfirmTxGasAndCalculate: sinon.spy(), + updateTransactionGasFees: sinon.spy(), someOtherDispatchProp: sinon.spy(), createSpeedUpTransaction: sinon.spy(), hideSidebar: sinon.spy(), @@ -192,18 +186,14 @@ describe('gas-modal-page-container container', () => { ).toStrictEqual('bar'); expect(result.someOwnProp).toStrictEqual(123); - expect( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - ).toStrictEqual(0); + expect(dispatchProps.updateTransactionGasFees.callCount).toStrictEqual(0); expect(dispatchProps.setGasData.callCount).toStrictEqual(0); expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0); expect(dispatchProps.hideModal.callCount).toStrictEqual(0); result.onSubmit(); - expect( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - ).toStrictEqual(1); + expect(dispatchProps.updateTransactionGasFees.callCount).toStrictEqual(1); expect(dispatchProps.setGasData.callCount).toStrictEqual(0); expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0); expect(dispatchProps.hideModal.callCount).toStrictEqual(1); @@ -236,18 +226,14 @@ describe('gas-modal-page-container container', () => { ).toStrictEqual('bar'); expect(result.someOwnProp).toStrictEqual(123); - expect( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - ).toStrictEqual(0); + expect(dispatchProps.updateTransactionGasFees.callCount).toStrictEqual(0); expect(dispatchProps.setGasData.callCount).toStrictEqual(0); expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0); expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(0); result.onSubmit('mockNewLimit', 'mockNewPrice'); - expect( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - ).toStrictEqual(0); + expect(dispatchProps.updateTransactionGasFees.callCount).toStrictEqual(0); expect(dispatchProps.setGasData.callCount).toStrictEqual(1); expect(dispatchProps.setGasData.getCall(0).args).toStrictEqual([ 'mockNewLimit', @@ -276,9 +262,7 @@ describe('gas-modal-page-container container', () => { result.onSubmit(); - expect( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - ).toStrictEqual(0); + expect(dispatchProps.updateTransactionGasFees.callCount).toStrictEqual(0); expect(dispatchProps.setGasData.callCount).toStrictEqual(0); expect(dispatchProps.useCustomGas.callCount).toStrictEqual(0); expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1); diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index ea59b7882..aaa28a056 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -2,6 +2,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import PageContainer from '../../../ui/page-container'; import { Tabs, Tab } from '../../../ui/tabs'; +import { + disconnectGasFeeEstimatePoller, + getGasFeeEstimatesAndStartPolling, +} from '../../../../store/actions'; import AdvancedTabContent from './advanced-tab-content'; import BasicTabContent from './basic-tab-content'; @@ -17,7 +21,6 @@ export default class GasModalPageContainer extends Component { updateCustomGasPrice: PropTypes.func, updateCustomGasLimit: PropTypes.func, insufficientBalance: PropTypes.bool, - fetchBasicGasEstimates: PropTypes.func, gasPriceButtonGroupProps: PropTypes.object, infoRowProps: PropTypes.shape({ originalTotalFiat: PropTypes.string, @@ -38,8 +41,29 @@ export default class GasModalPageContainer extends Component { customPriceIsExcessive: PropTypes.bool.isRequired, }; + constructor(props) { + super(props); + this.state = { + pollingToken: undefined, + }; + } + componentDidMount() { - this.props.fetchBasicGasEstimates(); + this._isMounted = true; + getGasFeeEstimatesAndStartPolling().then((pollingToken) => { + if (this._isMounted) { + this.setState({ pollingToken }); + } else { + disconnectGasFeeEstimatePoller(pollingToken); + } + }); + } + + componentWillUnmount() { + this._isMounted = false; + if (this.state.pollingToken) { + disconnectGasFeeEstimatePoller(this.state.pollingToken); + } } renderBasicTabContent(gasPriceButtonGroupProps) { diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 4d477f486..571544cf0 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -5,13 +5,11 @@ import { createRetryTransaction, createSpeedUpTransaction, hideSidebar, - updateTransaction, } from '../../../../store/actions'; import { setCustomGasPrice, setCustomGasLimit, resetCustomData, - fetchBasicGasEstimates, } from '../../../../ducks/gas/gas.duck'; import { getSendMaxModeState, @@ -40,8 +38,8 @@ import { getAveragePriceEstimateInHexWEI, isCustomPriceExcessive, getIsGasEstimatesFetched, - getIsCustomNetworkGasPriceFetched, getShouldShowFiat, + getIsCustomNetworkGasPriceFetched, } from '../../../../selectors'; import { @@ -59,6 +57,7 @@ import { import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; import { GAS_LIMITS } from '../../../../../shared/constants/gas'; +import { updateTransactionGasFees } from '../../../../ducks/metamask/metamask'; import GasModalPageContainer from './gas-modal-page-container.component'; const mapStateToProps = (state, ownProps) => { @@ -116,9 +115,8 @@ const mapStateToProps = (state, ownProps) => { const balance = getCurrentEthBalance(state); const isMainnet = getIsMainnet(state); - const showFiat = getShouldShowFiat(state); - const isTestnet = getIsTestnet(state); + const showFiat = getShouldShowFiat(state); const newTotalEth = maxModeOn && asset.type === ASSET_TYPES.NATIVE @@ -209,6 +207,9 @@ const mapDispatchToProps = (dispatch) => { }, hideModal: () => dispatch(hideModal()), useCustomGas: () => dispatch(useCustomGas()), + updateTransactionGasFees: (gasFees) => { + dispatch(updateTransactionGasFees({ ...gasFees, expectHexWei: true })); + }, updateCustomGasPrice, updateCustomGasLimit: (newLimit) => dispatch(setCustomGasLimit(addHexPrefix(newLimit))), @@ -216,19 +217,13 @@ const mapDispatchToProps = (dispatch) => { dispatch(updateGasLimit(newLimit)); dispatch(updateGasPrice(newPrice)); }, - updateConfirmTxGasAndCalculate: (gasLimit, gasPrice, updatedTx) => { - updateCustomGasPrice(gasPrice); - dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16)))); - return dispatch(updateTransaction(updatedTx)); + createRetryTransaction: (txId, customGasSettings) => { + return dispatch(createRetryTransaction(txId, customGasSettings)); }, - createRetryTransaction: (txId, gasPrice, gasLimit) => { - return dispatch(createRetryTransaction(txId, gasPrice, gasLimit)); - }, - createSpeedUpTransaction: (txId, gasPrice, gasLimit) => { - return dispatch(createSpeedUpTransaction(txId, gasPrice, gasLimit)); + createSpeedUpTransaction: (txId, customGasSettings) => { + return dispatch(createSpeedUpTransaction(txId, customGasSettings)); }, hideSidebar: () => dispatch(hideSidebar()), - fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), }; }; @@ -248,9 +243,9 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const { useCustomGas: dispatchUseCustomGas, setGasData: dispatchSetGasData, - updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, createRetryTransaction: dispatchCreateRetryTransaction, + updateTransactionGasFees: dispatchUpdateTransactionGasFees, hideSidebar: dispatchHideSidebar, cancelAndClose: dispatchCancelAndClose, hideModal: dispatchHideModal, @@ -265,26 +260,24 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { if (ownProps.onSubmit) { dispatchHideSidebar(); dispatchCancelAndClose(); - ownProps.onSubmit(gasLimit, gasPrice); + ownProps.onSubmit({ gasLimit, gasPrice }); return; } if (isConfirm) { - const updatedTx = { - ...transaction, - txParams: { - ...transaction.txParams, - gas: gasLimit, - gasPrice, - }, - }; - dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice, updatedTx); + dispatchUpdateTransactionGasFees({ + gasLimit, + gasPrice, + transaction, + isModal: true, + }); dispatchHideModal(); + dispatchCancelAndClose(); } else if (isSpeedUp) { - dispatchCreateSpeedUpTransaction(txId, gasPrice, gasLimit); + dispatchCreateSpeedUpTransaction(txId, { gasPrice, gasLimit }); dispatchHideSidebar(); dispatchCancelAndClose(); } else if (isRetry) { - dispatchCreateRetryTransaction(txId, gasPrice, gasLimit); + dispatchCreateRetryTransaction(txId, { gasPrice, gasLimit }); dispatchHideSidebar(); dispatchCancelAndClose(); } else { diff --git a/ui/components/app/gas-customization/gas-modal-page-container/index.scss b/ui/components/app/gas-customization/gas-modal-page-container/index.scss index be2dcc3ed..fe069251c 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/index.scss +++ b/ui/components/app/gas-customization/gas-modal-page-container/index.scss @@ -36,7 +36,7 @@ color: #4eade7; position: absolute; font-size: 0.75rem; - top: 4px; + top: 8px; right: 16px; cursor: pointer; overflow: hidden; diff --git a/ui/components/app/gas-timing/gas-timing.component.js b/ui/components/app/gas-timing/gas-timing.component.js new file mode 100644 index 000000000..788a6262a --- /dev/null +++ b/ui/components/app/gas-timing/gas-timing.component.js @@ -0,0 +1,77 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates'; +import { I18nContext } from '../../../contexts/i18n'; + +import Typography from '../../ui/typography/typography'; +import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; + +// Once we reach this second threshold, we switch to minutes as a unit +const SECOND_CUTOFF = 90; + +export default function GasTiming({ maxPriorityFeePerGas }) { + const { gasFeeEstimates, isGasEstimatesLoading } = useGasFeeEstimates(); + + const t = useContext(I18nContext); + + // Shows "seconds" as unit of time if under SECOND_CUTOFF, otherwise "minutes" + const toHumanReadableTime = (milliseconds = 1) => { + const seconds = Math.ceil(milliseconds / 1000); + if (seconds <= SECOND_CUTOFF) { + return t('gasTimingSeconds', [seconds]); + } + return t('gasTimingMinutes', [Math.ceil(seconds / 60)]); + }; + + // Don't show anything if we don't have enough information + if (isGasEstimatesLoading) { + return null; + } + + const { low, medium, high } = gasFeeEstimates; + + let text = ''; + let attitude = ''; + + // Anything medium or faster is positive + if ( + Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas) + ) { + attitude = 'positive'; + + // High+ is very likely, medium is likely + if ( + Number(maxPriorityFeePerGas) < Number(high.suggestedMaxPriorityFeePerGas) + ) { + text = t('gasTimingPositive', [ + toHumanReadableTime(medium.maxWaitTimeEstimate), + ]); + } else { + text = t('gasTimingVeryPositive', [ + toHumanReadableTime(high.maxWaitTimeEstimate), + ]); + } + } else { + attitude = 'negative'; + text = t('gasTimingNegative', [ + toHumanReadableTime(low.maxWaitTimeEstimate), + ]); + } + + return ( + + {text} + + ); +} + +GasTiming.propTypes = { + maxPriorityFeePerGas: PropTypes.string.isRequired, +}; diff --git a/ui/components/app/gas-timing/gas-timing.component.test.js b/ui/components/app/gas-timing/gas-timing.component.test.js new file mode 100644 index 000000000..38c4c5d60 --- /dev/null +++ b/ui/components/app/gas-timing/gas-timing.component.test.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; + +import messages from '../../../../app/_locales/en/messages.json'; + +import { getMessage } from '../../../helpers/utils/i18n-helper'; + +import * as i18nhooks from '../../../hooks/useI18nContext'; +import * as useGasFeeEstimatesExport from '../../../hooks/useGasFeeEstimates'; + +import GasTiming from '.'; + +const MOCK_FEE_ESTIMATE = { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', +}; + +describe('Gas timing', () => { + let useI18nContext; + + beforeEach(() => { + useI18nContext = sinon.stub(i18nhooks, 'useI18nContext'); + useI18nContext.returns((key, variables) => + getMessage('en', messages, key, variables), + ); + }); + afterEach(() => { + sinon.restore(); + }); + + it('renders nothing when gas is loading', () => { + sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ + isGasEstimatesLoading: true, + gasFeeEstimates: null, + }); + + const wrapper = shallow(); + expect(wrapper.html()).toBeNull(); + }); + + it('renders "very likely" when high estimate is chosen', () => { + sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }); + + const wrapper = shallow(); + expect(wrapper.html()).toContain('gasTimingVeryPositive'); + }); + + it('renders "likely" when medium estimate is chosen', () => { + sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }); + + const wrapper = shallow(); + expect(wrapper.html()).toContain('gasTimingPositive'); + }); + + it('renders "maybe" when low estimate is chosen', () => { + sinon.stub(useGasFeeEstimatesExport, 'useGasFeeEstimates').returns({ + isGasEstimatesLoading: false, + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }); + + const wrapper = shallow(); + expect(wrapper.html()).toContain('gasTimingNegative'); + }); +}); diff --git a/ui/components/app/gas-timing/index.js b/ui/components/app/gas-timing/index.js new file mode 100644 index 000000000..35e38614a --- /dev/null +++ b/ui/components/app/gas-timing/index.js @@ -0,0 +1 @@ +export { default } from './gas-timing.component'; diff --git a/ui/components/app/gas-timing/index.scss b/ui/components/app/gas-timing/index.scss new file mode 100644 index 000000000..b8174b982 --- /dev/null +++ b/ui/components/app/gas-timing/index.scss @@ -0,0 +1,20 @@ +.typography.gas-timing { + color: $ui-4; + + &--positive { + color: $success-3; + } + + &--warning { + color: $alert-3; + } + + &--negative { + color: $error-1; + } + + .info-tooltip { + display: inline-block; + margin-inline-start: 4px; + } +} diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index 3ae13c385..fa2d1b377 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -32,6 +32,15 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { const selectedIdentity = useSelector(getSelectedIdentity); const { address } = selectedIdentity; const addressLink = getAccountLink(address, chainId, rpcPrefs); + const { blockExplorerUrl } = rpcPrefs; + + const getBlockExplorerUrlHost = () => { + try { + return new URL(blockExplorerUrl)?.hostname; + } catch (err) { + return ''; + } + }; const openFullscreenEvent = useMetricEvent({ eventOpts: { @@ -67,6 +76,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { }); const isRemovable = keyring.type !== 'HD Key Tree'; + const blockExplorerUrlSubTitle = getBlockExplorerUrlHost(); return ( - {rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/u)[1]} + {blockExplorerUrlSubTitle} ) : null } diff --git a/ui/components/app/modals/account-modal-container/index.scss b/ui/components/app/modals/account-modal-container/index.scss index ad4ca6118..bfb0c37a8 100644 --- a/ui/components/app/modals/account-modal-container/index.scss +++ b/ui/components/app/modals/account-modal-container/index.scss @@ -34,7 +34,7 @@ color: $dusty-gray; position: absolute; cursor: pointer; - top: 10px; + top: -10px; right: 12px; &::after { diff --git a/ui/components/app/modals/cancel-transaction/cancel-transaction.container.js b/ui/components/app/modals/cancel-transaction/cancel-transaction.container.js index f05ecdf51..6e16a9d32 100644 --- a/ui/components/app/modals/cancel-transaction/cancel-transaction.container.js +++ b/ui/components/app/modals/cancel-transaction/cancel-transaction.container.js @@ -10,8 +10,7 @@ const mapStateToProps = (state, ownProps) => { transactionId, originalGasPrice, newGasFee, - defaultNewGasPrice, - gasLimit, + customGasSettings, } = ownProps; const { currentNetworkTxList } = metamask; const transaction = currentNetworkTxList.find( @@ -23,18 +22,15 @@ const mapStateToProps = (state, ownProps) => { transactionId, transactionStatus, originalGasPrice, - defaultNewGasPrice, + customGasSettings, newGasFee, - gasLimit, }; }; const mapDispatchToProps = (dispatch) => { return { - createCancelTransaction: (txId, customGasPrice, customGasLimit) => { - return dispatch( - createCancelTransaction(txId, customGasPrice, customGasLimit), - ); + createCancelTransaction: (txId, customGasSettings) => { + return dispatch(createCancelTransaction(txId, customGasSettings)); }, showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })), @@ -42,12 +38,7 @@ const mapDispatchToProps = (dispatch) => { }; const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { - transactionId, - defaultNewGasPrice, - gasLimit, - ...restStateProps - } = stateProps; + const { transactionId, customGasSettings, ...restStateProps } = stateProps; // eslint-disable-next-line no-shadow const { createCancelTransaction, ...restDispatchProps } = dispatchProps; @@ -56,7 +47,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...restDispatchProps, ...ownProps, createCancelTransaction: () => - createCancelTransaction(transactionId, defaultNewGasPrice, gasLimit), + createCancelTransaction(transactionId, customGasSettings), }; }; diff --git a/ui/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js b/ui/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js index 3f1775a47..59fb89831 100644 --- a/ui/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js +++ b/ui/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js @@ -14,6 +14,7 @@ export default class DepositEtherModal extends Component { isTestnet: PropTypes.bool.isRequired, isMainnet: PropTypes.bool.isRequired, toWyre: PropTypes.func.isRequired, + toTransak: PropTypes.func.isRequired, address: PropTypes.string.isRequired, toFaucet: PropTypes.func.isRequired, hideWarning: PropTypes.func.isRequired, @@ -87,6 +88,7 @@ export default class DepositEtherModal extends Component { const { chainId, toWyre, + toTransak, address, toFaucet, isTestnet, @@ -138,6 +140,31 @@ export default class DepositEtherModal extends Component { }, hide: !isMainnet, })} + {this.renderRow({ + logo: ( +
    + ), + title: this.context.t('buyWithTransak'), + text: this.context.t('buyWithTransakDescription'), + buttonLabel: this.context.t('continueToTransak'), + onButtonClick: () => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Deposit Ether', + name: 'Click buy Ether via Transak', + }, + }); + toTransak(address); + }, + hide: !isMainnet, + })} {this.renderRow({ logo: ( { dispatch(buyEth({ service: 'wyre', address })); }, + toTransak: (address) => { + dispatch(buyEth({ service: 'transak', address })); + }, hideModal: () => { dispatch(hideModal()); }, diff --git a/ui/components/app/modals/export-private-key-modal/index.scss b/ui/components/app/modals/export-private-key-modal/index.scss index 737b94863..a0862f5bc 100644 --- a/ui/components/app/modals/export-private-key-modal/index.scss +++ b/ui/components/app/modals/export-private-key-modal/index.scss @@ -84,6 +84,8 @@ display: flex; flex-direction: row; justify-content: center; + width: 100%; + padding: 0 25px; } &__button { diff --git a/ui/components/app/modals/loading-network-error/loading-network-error.component.js b/ui/components/app/modals/loading-network-error/loading-network-error.component.js index 0b473f5f4..17c45376e 100644 --- a/ui/components/app/modals/loading-network-error/loading-network-error.component.js +++ b/ui/components/app/modals/loading-network-error/loading-network-error.component.js @@ -8,7 +8,7 @@ const LoadingNetworkError = (props, context) => { return ( hideModal()} submitText={t('tryAgain')}> - + ); }; diff --git a/ui/components/app/modals/modal.js b/ui/components/app/modals/modal.js index ce4282df3..96352bc19 100644 --- a/ui/components/app/modals/modal.js +++ b/ui/components/app/modals/modal.js @@ -11,6 +11,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; // Modal Components import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'; import SwapsGasCustomizationModal from '../../../pages/swaps/swaps-gas-customization-modal'; +import EditGasPopover from '../edit-gas-popover/edit-gas-popover.component'; import DepositEtherModal from './deposit-ether-modal'; import AccountDetailsModal from './account-details-modal'; import ExportPrivateKeyModal from './export-private-key-modal'; @@ -246,7 +247,11 @@ const MODALS = { }, CUSTOMIZE_GAS: { - contents: , + contents: process.env.SHOW_EIP_1559_UI ? ( + + ) : ( + + ), mobileModalStyle: { width: '100vw', height: '100vh', diff --git a/ui/components/app/multiple-notifications/index.scss b/ui/components/app/multiple-notifications/index.scss index ea17e9467..01d3af889 100644 --- a/ui/components/app/multiple-notifications/index.scss +++ b/ui/components/app/multiple-notifications/index.scss @@ -55,9 +55,9 @@ .home-notification-wrapper--show-first { > div { - position: absolute; - bottom: 0; - right: 0; + position: fixed; + bottom: 10px; + right: 10px; visibility: hidden; } diff --git a/ui/components/app/network-display/index.scss b/ui/components/app/network-display/index.scss index fdfdc3302..78fe71f56 100644 --- a/ui/components/app/network-display/index.scss +++ b/ui/components/app/network-display/index.scss @@ -5,7 +5,6 @@ padding: 0 10px; border-radius: 4px; min-height: 25px; - cursor: pointer; user-select: none; &--disabled { @@ -36,7 +35,6 @@ background-color: lighten($dodger-blue, 35%); } - & .chip__label { overflow: hidden; text-overflow: ellipsis; @@ -56,4 +54,8 @@ width: 12px; display: block; } + + &--clickable { + cursor: pointer; + } } diff --git a/ui/components/app/network-display/network-display.js b/ui/components/app/network-display/network-display.js index 87b851535..935f36c53 100644 --- a/ui/components/app/network-display/network-display.js +++ b/ui/components/app/network-display/network-display.js @@ -74,6 +74,7 @@ export default function NetworkDisplay({ 'network-display--colored': colored, 'network-display--disabled': disabled, [`network-display--${networkType}`]: colored && networkType, + 'network-display--clickable': typeof onClick === 'function', })} labelProps={{ variant: TYPOGRAPHY.H7, diff --git a/ui/components/app/selected-account/index.scss b/ui/components/app/selected-account/index.scss index 05757b73e..acb95b0df 100644 --- a/ui/components/app/selected-account/index.scss +++ b/ui/components/app/selected-account/index.scss @@ -26,6 +26,8 @@ @include H7; color: #989a9b; + display: flex; + align-items: center; } &__clickable { @@ -48,4 +50,10 @@ background-color: #d9d7da; } } + + &__copy { + height: 13px; + display: inline-block; + margin-inline-start: 3px; + } } diff --git a/ui/components/app/selected-account/selected-account.component.js b/ui/components/app/selected-account/selected-account.component.js index 2c4df0ce0..3e505d560 100644 --- a/ui/components/app/selected-account/selected-account.component.js +++ b/ui/components/app/selected-account/selected-account.component.js @@ -4,6 +4,7 @@ import copyToClipboard from 'copy-to-clipboard'; import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../ui/tooltip'; +import CopyIcon from '../../ui/icon/copy-icon.component'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { SECOND } from '../../../../shared/constants/time'; @@ -61,6 +62,9 @@ class SelectedAccount extends Component {
    {shortenAddress(checksummedAddress)} +
    + +
    diff --git a/ui/components/app/sidebars/sidebar.component.js b/ui/components/app/sidebars/sidebar.component.js index 45be23b50..4ed71461b 100644 --- a/ui/components/app/sidebars/sidebar.component.js +++ b/ui/components/app/sidebars/sidebar.component.js @@ -15,6 +15,10 @@ export default class Sidebar extends Component { onOverlayClose: PropTypes.func, }; + static contextTypes = { + t: PropTypes.func, + }; + renderOverlay() { const { onOverlayClose } = this.props; @@ -57,6 +61,8 @@ export default class Sidebar extends Component { render() { const { transitionName, sidebarOpen, sidebarShouldClose } = this.props; + const showSidebar = sidebarOpen && !sidebarShouldClose; + return (
    - {sidebarOpen && !sidebarShouldClose - ? this.renderSidebarContent() - : null} + {showSidebar ? this.renderSidebarContent() : null} - {sidebarOpen && !sidebarShouldClose ? this.renderOverlay() : null} + {showSidebar ? this.renderOverlay() : null}
    ); } diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index 091916cc5..c999cf282 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -11,7 +11,7 @@ import { import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import Identicon from '../../ui/identicon'; import AccountListItem from '../account-list-item'; -import { conversionUtil } from '../../../helpers/utils/conversion-util'; +import { conversionUtil } from '../../../../shared/modules/conversion.utils'; import Button from '../../ui/button'; import SiteIcon from '../../ui/site-icon'; diff --git a/ui/components/app/step-progress-bar/index.js b/ui/components/app/step-progress-bar/index.js new file mode 100644 index 000000000..8a02cf42f --- /dev/null +++ b/ui/components/app/step-progress-bar/index.js @@ -0,0 +1 @@ +export { default } from './step-progress-bar'; diff --git a/ui/components/app/step-progress-bar/index.scss b/ui/components/app/step-progress-bar/index.scss new file mode 100644 index 000000000..b2a35bef1 --- /dev/null +++ b/ui/components/app/step-progress-bar/index.scss @@ -0,0 +1,66 @@ +.progressbar { + counter-reset: step; + display: flex; + justify-content: space-evenly; +} + +.progressbar li { + list-style-type: none; + width: 25%; + float: left; + font-size: 12px; + position: relative; + text-align: center; + text-transform: uppercase; + color: #7d7d7d; + z-index: 2; +} + +.progressbar li::before { + width: 30px; + height: 30px; + content: counter(step); + counter-increment: step; + line-height: 30px; + border: 2px solid #d6d9dc; + display: block; + text-align: center; + margin: 0 auto 10px auto; + border-radius: 50%; + background-color: white; + z-index: -1; +} + +.progressbar li::after { + width: 100%; + height: 2px; + content: ''; + position: absolute; + background-color: #d6d9dc; + top: 15px; + right: 62%; + z-index: -1; +} + +.progressbar li:first-child::after { + content: none; +} + +.progressbar li.active { + color: $primary-blue; +} + +.progressbar li.active::before { + border-color: $primary-blue; + z-index: 1; +} + +.progressbar li.active + li::after { + background-color: $primary-blue; + z-index: -1; +} + +.progressbar li.complete::before { + background-color: $primary-blue; + color: $ui-white; +} diff --git a/ui/components/app/step-progress-bar/step-progress-bar.js b/ui/components/app/step-progress-bar/step-progress-bar.js new file mode 100644 index 000000000..72e80319c --- /dev/null +++ b/ui/components/app/step-progress-bar/step-progress-bar.js @@ -0,0 +1,50 @@ +import React from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import Box from '../../ui/box'; + +const stages = { + PASSWORD_CREATE: 1, + SEED_PHRASE_VIDEO: 2, + SEED_PHRASE_REVIEW: 3, + SEED_PHRASE_CONFIRM: 4, + ONBOARDING_COMPLETE: 5, +}; +export default function StepProgressBar({ stage = 'PASSWORD_CREATE' }) { + const t = useI18nContext(); + return ( + +
      +
    • = 1, + complete: stages[stage] >= 1, + })} + > + {t('createPassword')} +
    • +
    • = 2, + complete: stages[stage] >= 3, + })} + > + {t('secureWallet')} +
    • +
    • = 4, + complete: stages[stage] >= 5, + })} + > + {t('confirmSeedPhrase')} +
    • +
    +
    + ); +} + +StepProgressBar.propTypes = { + stage: PropTypes.string, +}; diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.component.js b/ui/components/app/transaction-breakdown/transaction-breakdown.component.js index 4698819cf..3ebd3d20c 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.component.js @@ -23,6 +23,9 @@ export default class TransactionBreakdown extends PureComponent { gasPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), gasUsed: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), totalInHex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + baseFee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + priorityFee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + effectiveGasPrice: PropTypes.number, }; static defaultProps = { @@ -42,11 +45,14 @@ export default class TransactionBreakdown extends PureComponent { totalInHex, gasUsed, isTokenApprove, + baseFee, + priorityFee, + effectiveGasPrice, } = this.props; return (
    {t('transaction')}
    - + {typeof nonce === 'undefined' ? null : ( )} - - {typeof gasPrice === 'undefined' ? ( - '?' - ) : ( - + {typeof baseFee === 'undefined' ? ( + '?' + ) : ( + + )} + + )} + {process.env.SHOW_EIP_1559_UI && ( + + {typeof priorityFee === 'undefined' ? ( + '?' + ) : ( + + )} + + )} + {!process.env.SHOW_EIP_1559_UI && ( + + {typeof gasPrice === 'undefined' ? ( + '?' + ) : ( + + )} + + )} + + + {showFiat && ( + )} -
    + + {showFiat && ( - {showFiat && ( - - )} -
    + )}
    ); diff --git a/ui/components/app/transaction-detail-item/index.js b/ui/components/app/transaction-detail-item/index.js new file mode 100644 index 000000000..e37141eed --- /dev/null +++ b/ui/components/app/transaction-detail-item/index.js @@ -0,0 +1 @@ +export { default } from './transaction-detail-item.component'; diff --git a/ui/components/app/transaction-detail-item/index.scss b/ui/components/app/transaction-detail-item/index.scss new file mode 100644 index 000000000..ec8286df3 --- /dev/null +++ b/ui/components/app/transaction-detail-item/index.scss @@ -0,0 +1,38 @@ +.transaction-detail-item { + color: $ui-4; + + &__row { + display: flex; + } + + &__title { + flex-grow: 1; + word-break: break-word; + } + + .info-tooltip { + display: inline-block; + margin-inline-start: 4px; + } + + &__detail-text { + margin-inline-end: 20px !important; + } + + &__total { + font-weight: bold; + color: $ui-black; + } + + &__subtitle { + flex-grow: 1; + } + + &__subtext { + text-align: end; + } + + .currency-display-component { + display: inline; + } +} diff --git a/ui/components/app/transaction-detail-item/transaction-detail-item.component.js b/ui/components/app/transaction-detail-item/transaction-detail-item.component.js new file mode 100644 index 000000000..bfb446bd0 --- /dev/null +++ b/ui/components/app/transaction-detail-item/transaction-detail-item.component.js @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Typography from '../../ui/typography/typography'; +import { + COLORS, + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system'; + +export default function TransactionDetailItem({ + detailTitle = '', + detailText = '', + detailTitleColor = COLORS.BLACK, + detailTotal = '', + subTitle = '', + subText = '', +}) { + return ( +
    +
    + + {detailTitle} + + {detailText && ( + + {detailText} + + )} + + {detailTotal} + +
    +
    + + {subTitle} + + + + {subText} + +
    +
    + ); +} + +TransactionDetailItem.propTypes = { + detailTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + detailTitleColor: PropTypes.string, + detailText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + detailTotal: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + subTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + subText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; diff --git a/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js b/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js new file mode 100644 index 000000000..e8ca87cf7 --- /dev/null +++ b/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js @@ -0,0 +1,33 @@ +import React from 'react'; +import InfoTooltip from '../../ui/info-tooltip/info-tooltip'; +import GasTiming from '../gas-timing/gas-timing.component'; +import TransactionDetailItem from '.'; + +export default { + title: 'Transaction Detail Item', +}; + +export const basic = () => { + return ( +
    + + Estimated gas fee + + + + + } + detailText="16565.30" + detailTotal="0.0089 ETH" + subTitle={} + subText={ + <> + From $16565 - $19000 + + } + /> +
    + ); +}; diff --git a/ui/components/app/transaction-detail/index.js b/ui/components/app/transaction-detail/index.js new file mode 100644 index 000000000..7ee797f13 --- /dev/null +++ b/ui/components/app/transaction-detail/index.js @@ -0,0 +1 @@ +export { default } from './transaction-detail.component'; diff --git a/ui/components/app/transaction-detail/index.scss b/ui/components/app/transaction-detail/index.scss new file mode 100644 index 000000000..d5e732d5e --- /dev/null +++ b/ui/components/app/transaction-detail/index.scss @@ -0,0 +1,32 @@ +.transaction-detail { + position: relative; + + .transaction-detail-edit { + text-align: end; + padding-top: 20px; + + button { + @include H7; + + color: $primary-1; + background: transparent; + border: 0; + padding-inline-end: 0; + text-transform: uppercase; + } + } + + &-rows &-item:first-child { + padding-top: 0; + } + + &-item { + padding: 20px 0; + border-bottom: 1px solid $ui-3; + + &:last-child { + padding-bottom: 0; + border-bottom: 0; + } + } +} diff --git a/ui/components/app/transaction-detail/transaction-detail.component.js b/ui/components/app/transaction-detail/transaction-detail.component.js new file mode 100644 index 000000000..63594be25 --- /dev/null +++ b/ui/components/app/transaction-detail/transaction-detail.component.js @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { I18nContext } from '../../../contexts/i18n'; +import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; + +import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; + +export default function TransactionDetail({ rows = [], onEdit }) { + const t = useContext(I18nContext); + const shouldAnimate = useShouldAnimateGasEstimations(); + + return ( +
    + + {onEdit && ( +
    + +
    + )} +
    {rows}
    +
    + ); +} + +TransactionDetail.propTypes = { + rows: PropTypes.arrayOf(TransactionDetailItem).isRequired, + onEdit: PropTypes.func, +}; diff --git a/ui/components/app/transaction-detail/transaction-detail.stories.js b/ui/components/app/transaction-detail/transaction-detail.stories.js new file mode 100644 index 000000000..193090c25 --- /dev/null +++ b/ui/components/app/transaction-detail/transaction-detail.stories.js @@ -0,0 +1,59 @@ +import React from 'react'; +import InfoTooltip from '../../ui/info-tooltip/info-tooltip'; +import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; +import GasTiming from '../gas-timing/gas-timing.component'; +import TransactionDetail from '.'; + +export default { + title: 'Transaction Detail', +}; + +const rows = [ + + Estimated gas fee + + + + + } + detailText="0.00896 ETH" + detailTotal="$15.73" + subTitle={} + subText={ + <> + From $15.73 - $19.81 + + } + />, + + Up to $19.85 + + } + />, +]; + +export const basic = () => { + return ( +
    + +
    + ); +}; + +export const editable = () => { + return ( +
    + console.log('Edit!')} /> +
    + ); +}; diff --git a/ui/components/app/transaction-icon/transaction-icon.js b/ui/components/app/transaction-icon/transaction-icon.js index 6980a89cd..7aedfd7f4 100644 --- a/ui/components/app/transaction-icon/transaction-icon.js +++ b/ui/components/app/transaction-icon/transaction-icon.js @@ -1,6 +1,6 @@ import React from 'react'; +import { useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; -import { captureException } from '@sentry/browser'; import Approve from '../../ui/icon/approve-icon.component'; import Interaction from '../../ui/icon/interaction-icon.component'; import Receive from '../../ui/icon/receive-icon.component'; @@ -12,6 +12,7 @@ import { TRANSACTION_GROUP_STATUSES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; +import { captureSingleException } from '../../../store/actions'; const ICON_MAP = { [TRANSACTION_GROUP_CATEGORIES.APPROVAL]: Approve, @@ -34,20 +35,21 @@ const COLOR_MAP = { [TRANSACTION_STATUSES.REJECTED]: FAIL_COLOR, [TRANSACTION_GROUP_STATUSES.CANCELLED]: FAIL_COLOR, [TRANSACTION_STATUSES.DROPPED]: FAIL_COLOR, + [TRANSACTION_STATUSES.SUBMITTED]: PENDING_COLOR, }; export default function TransactionIcon({ status, category }) { - const color = COLOR_MAP[status] || OK_COLOR; + const dispatch = useDispatch(); + const color = COLOR_MAP[status] || OK_COLOR; const Icon = ICON_MAP[category]; if (!Icon) { - captureException( - Error( + dispatch( + captureSingleException( `The category prop passed to TransactionIcon is not supported. The prop is: ${category}`, ), ); - return
    ; } @@ -63,6 +65,8 @@ TransactionIcon.propTypes = { TRANSACTION_STATUSES.REJECTED, TRANSACTION_GROUP_STATUSES.CANCELLED, TRANSACTION_STATUSES.DROPPED, + TRANSACTION_STATUSES.CONFIRMED, + TRANSACTION_STATUSES.SUBMITTED, ]).isRequired, category: PropTypes.oneOf([ TRANSACTION_GROUP_CATEGORIES.APPROVAL, diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index 98635cce4..5065ae957 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -18,6 +18,8 @@ import { TRANSACTION_GROUP_CATEGORIES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; +import { EDIT_GAS_MODES } from '../../../../shared/constants/gas'; +import EditGasPopover from '../edit-gas-popover'; export default function TransactionListItem({ transactionGroup, @@ -32,10 +34,15 @@ export default function TransactionListItem({ initialTransaction: { id }, primaryTransaction: { err, status }, } = transactionGroup; - const [cancelEnabled, cancelTransaction] = useCancelTransaction( - transactionGroup, - ); - const retryTransaction = useRetryTransaction(transactionGroup); + const [ + cancelEnabled, + { cancelTransaction, showCancelEditGasPopover, closeCancelEditGasPopover }, + ] = useCancelTransaction(transactionGroup); + const { + retryTransaction, + showRetryEditGasPopover, + closeRetryEditGasPopover, + } = useRetryTransaction(transactionGroup); const shouldShowSpeedUp = useShouldShowSpeedUp( transactionGroup, isEarliestNonce, @@ -203,6 +210,20 @@ export default function TransactionListItem({ cancelDisabled={!cancelEnabled} /> )} + {process.env.SHOW_EIP_1559_UI && showRetryEditGasPopover && ( + + )} + {process.env.SHOW_EIP_1559_UI && showCancelEditGasPopover && ( + + )} ); } diff --git a/ui/components/app/transaction-total-banner/index.js b/ui/components/app/transaction-total-banner/index.js new file mode 100644 index 000000000..42ccf75f4 --- /dev/null +++ b/ui/components/app/transaction-total-banner/index.js @@ -0,0 +1 @@ +export { default } from './transaction-total-banner.component'; diff --git a/ui/components/app/transaction-total-banner/index.scss b/ui/components/app/transaction-total-banner/index.scss new file mode 100644 index 000000000..42979624c --- /dev/null +++ b/ui/components/app/transaction-total-banner/index.scss @@ -0,0 +1,7 @@ +.transaction-total-banner { + text-align: center; + + &__detail { + padding-bottom: 4px; + } +} diff --git a/ui/components/app/transaction-total-banner/transaction-total-banner.component.js b/ui/components/app/transaction-total-banner/transaction-total-banner.component.js new file mode 100644 index 000000000..7057f9be3 --- /dev/null +++ b/ui/components/app/transaction-total-banner/transaction-total-banner.component.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Typography from '../../ui/typography/typography'; +import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system'; + +export default function TransactionTotalBanner({ + total = '', + detail = '', + timing, +}) { + return ( +
    + + ~ {total} + + {detail && ( + + {detail} + + )} + {timing} +
    + ); +} + +TransactionTotalBanner.propTypes = { + total: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + detail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + timing: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; diff --git a/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js b/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js new file mode 100644 index 000000000..46838ae37 --- /dev/null +++ b/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; +import TransactionTotalBanner from '.'; + +export default { + title: 'Transaction Total Banner', +}; + +export const basic = () => { + return ( + + Up to $19.81 (0.01234 ETH) + + } + timing="Very likely in < 15 seconds" + /> + ); +}; diff --git a/ui/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap b/ui/components/ui/actionable-message/__snapshots__/actionable-message.test.js.snap similarity index 100% rename from ui/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap rename to ui/components/ui/actionable-message/__snapshots__/actionable-message.test.js.snap diff --git a/ui/pages/swaps/actionable-message/actionable-message.js b/ui/components/ui/actionable-message/actionable-message.js similarity index 87% rename from ui/pages/swaps/actionable-message/actionable-message.js rename to ui/components/ui/actionable-message/actionable-message.js index 2d4c9da5c..750852f31 100644 --- a/ui/pages/swaps/actionable-message/actionable-message.js +++ b/ui/components/ui/actionable-message/actionable-message.js @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import InfoTooltip from '../../../components/ui/info-tooltip'; +import InfoTooltip from '../info-tooltip'; +import InfoTooltipIcon from '../info-tooltip/info-tooltip-icon'; const CLASSNAME_WARNING = 'actionable-message--warning'; const CLASSNAME_DANGER = 'actionable-message--danger'; @@ -20,16 +21,20 @@ export default function ActionableMessage({ infoTooltipText = '', withRightButton = false, type = false, + useIcon = false, + iconFillColor = '', }) { const actionableMessageClassName = classnames( 'actionable-message', typeHash[type], withRightButton ? CLASSNAME_WITH_RIGHT_BUTTON : null, className, + { 'actionable-message--with-icon': useIcon }, ); return (
    + {useIcon && } {infoTooltipText && ( ( />
    ); + +export const withIcon = () => ( +
    + +
    +); diff --git a/ui/pages/swaps/actionable-message/actionable-message.test.js b/ui/components/ui/actionable-message/actionable-message.test.js similarity index 100% rename from ui/pages/swaps/actionable-message/actionable-message.test.js rename to ui/components/ui/actionable-message/actionable-message.test.js diff --git a/ui/pages/swaps/actionable-message/index.js b/ui/components/ui/actionable-message/index.js similarity index 100% rename from ui/pages/swaps/actionable-message/index.js rename to ui/components/ui/actionable-message/index.js diff --git a/ui/pages/swaps/actionable-message/index.scss b/ui/components/ui/actionable-message/index.scss similarity index 88% rename from ui/pages/swaps/actionable-message/index.scss rename to ui/components/ui/actionable-message/index.scss index 4838489d4..a4c03ca22 100644 --- a/ui/pages/swaps/actionable-message/index.scss +++ b/ui/components/ui/actionable-message/index.scss @@ -9,6 +9,21 @@ align-items: center; position: relative; + &--with-icon { + padding-inline-start: 32px; + } + + &--with-icon.actionable-message--warning { + justify-content: normal; + } + + svg { + width: 16px; + height: 16px; + position: absolute; + left: 8px; + } + @include H7; &__message { @@ -69,6 +84,7 @@ &--left-aligned { .actionable-message__message, .actionable-message__actions { + text-align: left; } } diff --git a/ui/components/ui/button-group/button-group.component.js b/ui/components/ui/button-group/button-group.component.js index d1f20e961..9cd5d9896 100644 --- a/ui/components/ui/button-group/button-group.component.js +++ b/ui/components/ui/button-group/button-group.component.js @@ -61,6 +61,7 @@ export default class ButtonGroup extends PureComponent { index === this.state.activeButtonIndex, }, )} + data-testid={`button-group__button${index}`} onClick={() => { this.handleButtonClick(index); child.props.onClick?.(); diff --git a/ui/components/ui/chip/chip-with-input.js b/ui/components/ui/chip/chip-with-input.js new file mode 100644 index 000000000..c6fad92d9 --- /dev/null +++ b/ui/components/ui/chip/chip-with-input.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { COLORS } from '../../../helpers/constants/design-system'; +import Chip from '.'; + +export function ChipWithInput({ + className, + borderColor = COLORS.UI1, + inputValue, + setInputValue, +}) { + return ( + + {setInputValue && ( + { + setInputValue(e.target.value); + }} + value={inputValue} + /> + )} + + ); +} + +ChipWithInput.propTypes = { + borderColor: PropTypes.oneOf(Object.values(COLORS)), + className: PropTypes.string, + inputValue: PropTypes.string, + setInputValue: PropTypes.func, +}; diff --git a/ui/components/ui/chip/chip.js b/ui/components/ui/chip/chip.js index 65a13b657..232195f6e 100644 --- a/ui/components/ui/chip/chip.js +++ b/ui/components/ui/chip/chip.js @@ -63,4 +63,6 @@ Chip.propTypes = { rightIcon: PropTypes.node, className: PropTypes.string, onClick: PropTypes.func, + inputValue: PropTypes.string, + setInputValue: PropTypes.func, }; diff --git a/ui/components/ui/chip/chip.scss b/ui/components/ui/chip/chip.scss index 911f5a8ce..02c4df6a5 100644 --- a/ui/components/ui/chip/chip.scss +++ b/ui/components/ui/chip/chip.scss @@ -37,7 +37,22 @@ } } + &--with-input &__input { + direction: ltr; + border: none; + background: transparent; + text-align: center; + width: 100%; + font-size: design-system.$font-size-h5; + + &:focus { + text-align: left; + } + &:focus-visible { + outline: none; + } + } &--with-right-icon { padding-right: 4px; diff --git a/ui/components/ui/chip/chip.stories.js b/ui/components/ui/chip/chip.stories.js index 690d97172..c05cf7bbd 100644 --- a/ui/components/ui/chip/chip.stories.js +++ b/ui/components/ui/chip/chip.stories.js @@ -1,10 +1,11 @@ /* eslint-disable react/prop-types */ -import React from 'react'; +import React, { useState } from 'react'; import { select, text } from '@storybook/addon-knobs'; import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system'; import ApproveIcon from '../icon/approve-icon.component'; import Identicon from '../identicon/identicon.component'; +import { ChipWithInput } from './chip-with-input'; import Chip from '.'; export default { @@ -80,3 +81,13 @@ export const WithBothIcons = () => ( } /> ); +export const WithInput = () => { + const [inputValue, setInputValue] = useState(''); + return ( + + ); +}; diff --git a/ui/components/ui/error-message/error-message.component.js b/ui/components/ui/error-message/error-message.component.js index 542ecc47b..1786e332a 100644 --- a/ui/components/ui/error-message/error-message.component.js +++ b/ui/components/ui/error-message/error-message.component.js @@ -12,7 +12,7 @@ const ErrorMessage = (props, context) => { alt="" className="error-message__icon" /> -
    {`ALERT: ${error}`}
    +
    {error}
    ); }; diff --git a/ui/components/ui/error-message/error-message.component.test.js b/ui/components/ui/error-message/error-message.component.test.js index 5e1f0669a..94c16de27 100644 --- a/ui/components/ui/error-message/error-message.component.test.js +++ b/ui/components/ui/error-message/error-message.component.test.js @@ -14,7 +14,7 @@ describe('ErrorMessage Component', () => { expect(wrapper.find('.error-message')).toHaveLength(1); expect(wrapper.find('.error-message__icon')).toHaveLength(1); expect(wrapper.find('.error-message__text').text()).toStrictEqual( - 'ALERT: This is an error.', + 'This is an error.', ); }); @@ -27,7 +27,7 @@ describe('ErrorMessage Component', () => { expect(wrapper.find('.error-message')).toHaveLength(1); expect(wrapper.find('.error-message__icon')).toHaveLength(1); expect(wrapper.find('.error-message__text').text()).toStrictEqual( - 'ALERT: translate testKey', + 'translate testKey', ); }); }); diff --git a/ui/components/ui/error-message/error-message.stories.js b/ui/components/ui/error-message/error-message.stories.js new file mode 100644 index 000000000..64114ea0f --- /dev/null +++ b/ui/components/ui/error-message/error-message.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { text } from '@storybook/addon-knobs'; +import ErrorMessage from '.'; + +export default { + title: 'ErrorMessage', +}; + +export const primaryType = () => ( + +); diff --git a/ui/components/ui/error-message/index.scss b/ui/components/ui/error-message/index.scss index eba500931..e355d9939 100644 --- a/ui/components/ui/error-message/index.scss +++ b/ui/components/ui/error-message/index.scss @@ -2,14 +2,14 @@ @include H7; min-height: 32px; - border: 1px solid $monzo; - color: $monzo; - background: lighten($monzo, 56%); - border-radius: 4px; + border: 1px solid $Red-300; + color: $ui-black; + background: $error-2; + border-radius: 8px; display: flex; justify-content: flex-start; align-items: center; - padding: 8px 16px; + padding: 8px 10px; &__icon { margin-right: 8px; diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js new file mode 100644 index 000000000..5a4f97ab2 --- /dev/null +++ b/ui/components/ui/form-field/form-field.js @@ -0,0 +1,134 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Typography from '../typography/typography'; +import Box from '../box/box'; +import { + COLORS, + TEXT_ALIGN, + DISPLAY, + TYPOGRAPHY, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; + +import NumericInput from '../numeric-input/numeric-input.component'; +import InfoTooltip from '../info-tooltip/info-tooltip'; + +export default function FormField({ + titleText, + titleUnit, + tooltipText, + titleDetail, + error, + onChange, + value, + numeric, + detailText, + autoFocus, + password, +}) { + return ( +
    + +
    + ); +} + +FormField.propTypes = { + titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + titleUnit: PropTypes.string, + tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + titleDetail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + error: PropTypes.string, + onChange: PropTypes.func, + value: PropTypes.number, + detailText: PropTypes.string, + autoFocus: PropTypes.bool, + numeric: PropTypes.bool, + password: PropTypes.bool, +}; + +FormField.defaultProps = { + titleText: '', + titleUnit: '', + tooltipText: '', + titleDetail: '', + error: '', + onChange: undefined, + value: 0, + detailText: '', + autoFocus: false, + numeric: false, + password: false, +}; diff --git a/ui/components/ui/form-field/form-field.stories.js b/ui/components/ui/form-field/form-field.stories.js new file mode 100644 index 000000000..b78c908fe --- /dev/null +++ b/ui/components/ui/form-field/form-field.stories.js @@ -0,0 +1,55 @@ +/* eslint-disable react/prop-types */ + +import React, { useState } from 'react'; +import { select } from '@storybook/addon-knobs'; +import FormField from '.'; + +export default { + title: 'FormField', +}; + +export const Plain = ({ ...props }) => { + const options = { text: false, numeric: true }; + const [value, setValue] = useState(''); + return ( +
    + +
    + ); +}; + +export const FormFieldWithTitleDetail = () => { + const [clicked, setClicked] = useState(false); + const detailOptions = { + text:
    Detail
    , + button: ( + + ), + checkmark: , + }; + return ( + + ); +}; + +export const FormFieldWithError = () => { + return ; +}; diff --git a/ui/components/ui/form-field/index.js b/ui/components/ui/form-field/index.js new file mode 100644 index 000000000..0bcf42e6e --- /dev/null +++ b/ui/components/ui/form-field/index.js @@ -0,0 +1 @@ +export { default } from './form-field'; diff --git a/ui/components/ui/form-field/index.scss b/ui/components/ui/form-field/index.scss new file mode 100644 index 000000000..d7d4cb0b7 --- /dev/null +++ b/ui/components/ui/form-field/index.scss @@ -0,0 +1,48 @@ +.form-field { + margin-bottom: 20px; + + &__heading { + display: flex; + margin-top: 4px; + } + + .info-tooltip { + display: inline-block; + } + + &__heading-detail { + flex-grow: 1; + align-self: center; + } + + &__error, + &__error h6 { + color: $error-1 !important; + padding-top: 6px; + } + + h6 { + padding-bottom: 6px; + margin-inline-end: 6px; + } + + i { + color: #dadada; + font-size: $font-size-h7; + } + + &__input { + width: 100%; + border: solid 1px $ui-3; + padding: 10px; + border-radius: 6px; + + &:focus { + border: solid 2px $primary-1; + } + + &--error { + border-color: $error-1; + } + } +} diff --git a/ui/components/ui/info-tooltip/index.scss b/ui/components/ui/info-tooltip/index.scss index cdc52fb97..335108ffc 100644 --- a/ui/components/ui/info-tooltip/index.scss +++ b/ui/components/ui/info-tooltip/index.scss @@ -1,5 +1,5 @@ .info-tooltip { - img { + svg { height: 12px; width: 12px; } @@ -41,6 +41,18 @@ text-align: left; color: $Grey-500; + + a { + color: $primary-1; + } + + p { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } } } diff --git a/ui/components/ui/info-tooltip/info-tooltip-icon.js b/ui/components/ui/info-tooltip/info-tooltip-icon.js new file mode 100644 index 000000000..8e65c5870 --- /dev/null +++ b/ui/components/ui/info-tooltip/info-tooltip-icon.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function InfoTooltipIcon({ fillColor = '#b8b8b8' }) { + return ( + + + + ); +} + +InfoTooltipIcon.propTypes = { + fillColor: PropTypes.string, +}; diff --git a/ui/components/ui/info-tooltip/info-tooltip.js b/ui/components/ui/info-tooltip/info-tooltip.js index 4c67eefd0..03b0d96ca 100644 --- a/ui/components/ui/info-tooltip/info-tooltip.js +++ b/ui/components/ui/info-tooltip/info-tooltip.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Tooltip from '../tooltip'; +import InfoTooltipIcon from './info-tooltip-icon'; const positionArrowClassMap = { top: 'info-tooltip__top-tooltip-arrow', @@ -16,6 +17,7 @@ export default function InfoTooltip({ containerClassName, wrapperClassName, wide, + iconFillColor = '', }) { return (
    @@ -32,7 +34,7 @@ export default function InfoTooltip({ html={contentText} theme={wide ? 'tippy-tooltip-wideInfo' : 'tippy-tooltip-info'} > - +
    ); @@ -44,4 +46,5 @@ InfoTooltip.propTypes = { wide: PropTypes.bool, containerClassName: PropTypes.string, wrapperClassName: PropTypes.string, + iconFillColor: PropTypes.string, }; diff --git a/ui/components/ui/loading-heartbeat/index.js b/ui/components/ui/loading-heartbeat/index.js new file mode 100644 index 000000000..fae2e886d --- /dev/null +++ b/ui/components/ui/loading-heartbeat/index.js @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; + +export default function LoadingHeartBeat({ active }) { + const heartNode = useRef(null); + + const LOADING_CLASS = 'loading-heartbeat--active'; + + // When the loading animation completes, remove the className to disappear again + useEffect(() => { + const eventName = 'animationend'; + const node = heartNode?.current; + const eventHandler = () => { + node?.classList.remove(LOADING_CLASS); + }; + + node?.addEventListener(eventName, eventHandler); + return () => { + node?.removeEventListener(eventName, eventHandler); + }; + }, [heartNode]); + + return ( +
    + ); +} + +LoadingHeartBeat.propTypes = { + active: PropTypes.bool, +}; diff --git a/ui/components/ui/loading-heartbeat/index.scss b/ui/components/ui/loading-heartbeat/index.scss new file mode 100644 index 000000000..0b1b095f2 --- /dev/null +++ b/ui/components/ui/loading-heartbeat/index.scss @@ -0,0 +1,23 @@ +.loading-heartbeat { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + background: #fff; + display: none; + + &--active { + display: block; + animation: heartbeat 2s ease-in-out; + } +} + +@keyframes heartbeat { + 0% { opacity: 0; } + 25% { opacity: 1; } + 50% { opacity: 0.5; } + 75% { opacity: 1; } + 100% { opacity: 0; } +} diff --git a/ui/components/ui/numeric-input/index.js b/ui/components/ui/numeric-input/index.js new file mode 100644 index 000000000..338640323 --- /dev/null +++ b/ui/components/ui/numeric-input/index.js @@ -0,0 +1 @@ +export { default } from './numeric-input.component'; diff --git a/ui/components/ui/numeric-input/numeric-input.component.js b/ui/components/ui/numeric-input/numeric-input.component.js new file mode 100644 index 000000000..efc3ca44e --- /dev/null +++ b/ui/components/ui/numeric-input/numeric-input.component.js @@ -0,0 +1,48 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import Typography from '../typography/typography'; +import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system'; + +export default function NumericInput({ + detailText, + value, + onChange, + error, + autoFocus, +}) { + return ( +
    + onChange?.(parseInt(e.target.value, 10))} + min="0" + autoFocus={autoFocus} + /> + {detailText && ( + + {detailText} + + )} +
    + ); +} + +NumericInput.propTypes = { + value: PropTypes.number, + detailText: PropTypes.string, + onChange: PropTypes.func, + error: PropTypes.string, + autoFocus: PropTypes.bool, +}; + +NumericInput.defaultProps = { + value: 0, + detailText: '', + onChange: undefined, + error: '', + autoFocus: false, +}; diff --git a/ui/components/ui/numeric-input/numeric-input.scss b/ui/components/ui/numeric-input/numeric-input.scss new file mode 100644 index 000000000..f3ac6877d --- /dev/null +++ b/ui/components/ui/numeric-input/numeric-input.scss @@ -0,0 +1,28 @@ +.numeric-input { + border: 1px solid $ui-3; + position: relative; + border-radius: 6px; + + &--error { + border-color: $error-1; + } + + input { + width: 100%; + border: 0; + padding: 10px; + border-radius: 6px; + + /* ensures the increment/decrement arrows always display */ + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + } + } + + span { + position: absolute; + right: 40px; + top: 7px; + } +} diff --git a/ui/components/ui/numeric-input/numeric-input.stories.js b/ui/components/ui/numeric-input/numeric-input.stories.js new file mode 100644 index 000000000..53576c2d6 --- /dev/null +++ b/ui/components/ui/numeric-input/numeric-input.stories.js @@ -0,0 +1,36 @@ +import React from 'react'; +import NumericInput from '.'; + +export default { + title: 'NumericInput', +}; + +const onChange = (e) => console.log('changed value: ', e.target.value); + +export const numericInput = () => { + return ( +
    + +
    + ); +}; + +export const numericInputWithDetail = () => { + return ( +
    + +
    + ); +}; + +export const numericInputWithError = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/ui/page-container/page-container-header/page-container-header.component.js b/ui/components/ui/page-container/page-container-header/page-container-header.component.js index 3717b7450..0c3995c10 100644 --- a/ui/components/ui/page-container/page-container-header/page-container-header.component.js +++ b/ui/components/ui/page-container/page-container-header/page-container-header.component.js @@ -61,6 +61,7 @@ export default class PageContainerHeader extends Component { className={classnames('page-container__header', className, { 'page-container__header--no-padding-bottom': Boolean(tabs), })} + data-testid="page-container__header" > {this.renderHeaderRow()} diff --git a/ui/components/ui/radio-group/index.js b/ui/components/ui/radio-group/index.js new file mode 100644 index 000000000..64f0fbf15 --- /dev/null +++ b/ui/components/ui/radio-group/index.js @@ -0,0 +1 @@ +export { default } from './radio-group.component'; diff --git a/ui/components/ui/radio-group/index.scss b/ui/components/ui/radio-group/index.scss new file mode 100644 index 000000000..7aab3307d --- /dev/null +++ b/ui/components/ui/radio-group/index.scss @@ -0,0 +1,53 @@ +.radio-group { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 100px; + width: 300px; + + label { + cursor: pointer; + } + + &__column { + text-align: center; + } + + &__column-recommended { + height: 20px; + } + + &__column-line { + width: 1px; + height: 5px; + background-color: $ui-2; + margin: 0 auto; + } + + &__column-horizontal-line { + height: 1px; + background-color: $ui-2; + width: 100%; + } + + &__column:first-child &__column-horizontal-line { + width: 50px; + margin-left: 50px; + } + + &__column:last-child &__column-horizontal-line { + width: 51px; + } + + &__column-radio { + margin-inline-end: 1px; + } + + &__column-radio, + &__column-label { + text-align: center; + } + + &__column-label { + padding-top: 6px; + } +} diff --git a/ui/components/ui/radio-group/radio-group.component.js b/ui/components/ui/radio-group/radio-group.component.js new file mode 100644 index 000000000..c59b300f0 --- /dev/null +++ b/ui/components/ui/radio-group/radio-group.component.js @@ -0,0 +1,66 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { I18nContext } from '../../../contexts/i18n'; +import Typography from '../typography/typography'; +import { + COLORS, + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system'; + +export default function RadioGroup({ options, name, selectedValue, onChange }) { + const t = useContext(I18nContext); + + return ( +
    + {options.map((option) => { + const checked = option.value === selectedValue; + return ( +
    + +
    + ); + })} +
    + ); +} + +RadioGroup.propTypes = { + options: PropTypes.array, + selectedValue: PropTypes.string, + name: PropTypes.string, + onChange: PropTypes.func, +}; + +RadioGroup.defaultProps = { + options: [], +}; diff --git a/ui/components/ui/radio-group/radio-group.stories.js b/ui/components/ui/radio-group/radio-group.stories.js new file mode 100644 index 000000000..1d0aef252 --- /dev/null +++ b/ui/components/ui/radio-group/radio-group.stories.js @@ -0,0 +1,22 @@ +import React from 'react'; +import RadioGroup from '.'; + +export default { + title: 'RadioGroup', +}; + +export const radioGroup = () => { + return ( +
    + +
    + ); +}; diff --git a/ui/components/ui/token-input/token-input.component.js b/ui/components/ui/token-input/token-input.component.js index af11d86b5..5a879cb22 100644 --- a/ui/components/ui/token-input/token-input.component.js +++ b/ui/components/ui/token-input/token-input.component.js @@ -6,7 +6,7 @@ import { getWeiHexFromDecimalValue } from '../../../helpers/utils/conversions.ut import { conversionUtil, multiplyCurrencies, -} from '../../../helpers/utils/conversion-util'; +} from '../../../../shared/modules/conversion.utils'; import { ETH } from '../../../helpers/constants/common'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; diff --git a/ui/components/ui/typography/typography.js b/ui/components/ui/typography/typography.js index b18b53deb..ab3203ddd 100644 --- a/ui/components/ui/typography/typography.js +++ b/ui/components/ui/typography/typography.js @@ -25,9 +25,11 @@ export default function Typography({ 'typography', className, `typography--${variant}`, - `typography--align-${align}`, - `typography--color-${color}`, `typography--weight-${fontWeight}`, + { + [`typography--align-${align}`]: Boolean(align), + [`typography--color-${color}`]: Boolean(color), + }, ); let Tag = tag ?? variant; diff --git a/ui/components/ui/ui-components.scss b/ui/components/ui/ui-components.scss index 96d0c684a..f585f9052 100644 --- a/ui/components/ui/ui-components.scss +++ b/ui/components/ui/ui-components.scss @@ -1,5 +1,6 @@ /** Please import your files in alphabetical order **/ @import 'account-mismatch-warning/index'; +@import 'actionable-message/index'; @import 'alert-circle-icon/index'; @import 'alert/index'; @import 'box/box'; @@ -30,13 +31,17 @@ @import 'identicon/index'; @import 'info-tooltip/index'; @import 'list-item/index'; +@import 'loading-heartbeat/index'; @import 'loading-indicator/loading-indicator'; @import 'loading-screen/index'; @import 'menu/menu'; +@import 'numeric-input/numeric-input'; +@import 'form-field/index'; @import 'page-container/index'; @import 'popover/index'; @import 'pulse-loader/index'; @import 'qr-code/index'; +@import 'radio-group/index'; @import 'readonly-input/index'; @import 'sender-to-recipient/index'; @import 'snackbar/index'; diff --git a/ui/components/ui/unit-input/unit-input.component.js b/ui/components/ui/unit-input/unit-input.component.js index 78458cab6..8ea172ea7 100644 --- a/ui/components/ui/unit-input/unit-input.component.js +++ b/ui/components/ui/unit-input/unit-input.component.js @@ -45,6 +45,18 @@ export default class UnitInput extends PureComponent { this.unitInput.focus(); }; + handleInputFocus = ({ target: { value } }) => { + if (value === '0') { + this.setState({ value: '' }); + } + }; + + handleInputBlur = ({ target: { value } }) => { + if (value === '') { + this.setState({ value: '0' }); + } + }; + handleChange = (event) => { const { value: userInput } = event.target; let value = userInput; @@ -88,6 +100,8 @@ export default class UnitInput extends PureComponent { value={value} placeholder={placeholder} onChange={this.handleChange} + onBlur={this.handleInputBlur} + onFocus={this.handleInputFocus} style={{ width: this.getInputWidth(value) }} ref={(ref) => { this.unitInput = ref; diff --git a/ui/css/design-system/attributes.scss b/ui/css/design-system/attributes.scss index 2176cddb9..72fdcb51f 100644 --- a/ui/css/design-system/attributes.scss +++ b/ui/css/design-system/attributes.scss @@ -68,5 +68,5 @@ $sizes-strings: $border-style: solid, double, none, dashed, dotted; $directions: top, right, bottom, left; $display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item; -$text-align: left, right, center, justify; +$text-align: left, right, center, justify, end; $font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900; diff --git a/ui/css/itcss/components/send.scss b/ui/css/itcss/components/send.scss index 555692d81..9874b09ab 100644 --- a/ui/css/itcss/components/send.scss +++ b/ui/css/itcss/components/send.scss @@ -801,6 +801,7 @@ align-items: center; justify-content: center; color: #2f9ae0; + background: #fff; &__disabled { color: #b0d7f2; diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js index b19072d18..e3b3e275f 100644 --- a/ui/ducks/app/app.js +++ b/ui/ducks/app/app.js @@ -50,6 +50,9 @@ export default function reduceApp(state = {}, action) { openMetaMaskTabs: {}, currentWindowTab: {}, showWhatsNewPopup: true, + singleExceptions: { + testKey: null, + }, ...state, }; @@ -346,6 +349,15 @@ export default function reduceApp(state = {}, action) { showWhatsNewPopup: false, }; + case actionConstants.CAPTURE_SINGLE_EXCEPTION: + return { + ...appState, + singleExceptions: { + ...appState.singleExceptions, + [action.value]: null, + }, + }; + default: return appState; } diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.js index 09e74a2e8..d19d1111b 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.js @@ -3,7 +3,7 @@ import { currentCurrencySelector, unconfirmedTransactionsHashSelector, } from '../../selectors'; -import { getNativeCurrency } from '../metamask/metamask'; +import { getNativeCurrency, getTokens } from '../metamask/metamask'; import { getValueFromWeiHex, @@ -11,19 +11,19 @@ import { getHexGasTotal, addFiat, addEth, - increaseLastGasPrice, - hexGreaterThan, } from '../../helpers/utils/confirm-tx.util'; import { getTokenData, sumHexes } from '../../helpers/utils/transactions.util'; -import { conversionUtil } from '../../helpers/utils/conversion-util'; +import { conversionUtil } from '../../../shared/modules/conversion.utils'; +import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas'; // Actions const createActionType = (action) => `metamask/confirm-transaction/${action}`; const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA'); const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA'); +const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS'); const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION'); const UPDATE_TRANSACTION_AMOUNTS = createActionType( 'UPDATE_TRANSACTION_AMOUNTS', @@ -36,6 +36,7 @@ const UPDATE_NONCE = createActionType('UPDATE_NONCE'); const initState = { txData: {}, tokenData: {}, + tokenProps: {}, fiatTransactionAmount: '', fiatTransactionFee: '', fiatTransactionTotal: '', @@ -65,6 +66,13 @@ export default function reducer(state = initState, action = {}) { ...action.payload, }, }; + case UPDATE_TOKEN_PROPS: + return { + ...state, + tokenProps: { + ...action.payload, + }, + }; case UPDATE_TRANSACTION_AMOUNTS: { const { fiatTransactionAmount, @@ -135,6 +143,13 @@ export function updateTokenData(tokenData) { }; } +export function updateTokenProps(tokenProps) { + return { + type: UPDATE_TOKEN_PROPS, + payload: tokenProps, + }; +} + export function updateTransactionAmounts(amounts) { return { type: UPDATE_TRANSACTION_AMOUNTS, @@ -163,32 +178,6 @@ export function updateNonce(nonce) { }; } -function increaseFromLastGasPrice(txData) { - const { - lastGasPrice, - txParams: { gasPrice: previousGasPrice } = {}, - } = txData; - - // Set the minimum to a 10% increase from the lastGasPrice. - const minimumGasPrice = increaseLastGasPrice(lastGasPrice); - const gasPriceBelowMinimum = hexGreaterThan( - minimumGasPrice, - previousGasPrice, - ); - const gasPrice = - !previousGasPrice || gasPriceBelowMinimum - ? minimumGasPrice - : previousGasPrice; - - return { - ...txData, - txParams: { - ...txData.txParams, - gasPrice, - }, - }; -} - export function updateTxDataAndCalculate(txData) { return (dispatch, getState) => { const state = getState(); @@ -198,9 +187,14 @@ export function updateTxDataAndCalculate(txData) { dispatch(updateTxData(txData)); - const { - txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {}, - } = txData; + const { txParams: { value = '0x0', gas: gasLimit = '0x0' } = {} } = txData; + + // if the gas price from our infura endpoint is null or undefined + // use the metaswap average price estimation as a fallback + let { txParams: { gasPrice } = {} } = txData; + if (!gasPrice) { + gasPrice = getAveragePriceEstimateInHexWEI(state) || '0x0'; + } const fiatTransactionAmount = getValueFromWeiHex({ value, @@ -281,18 +275,24 @@ export function setTransactionToConfirm(transactionId) { } if (transaction.txParams) { - const { lastGasPrice } = transaction; - const txData = lastGasPrice - ? increaseFromLastGasPrice(transaction) - : transaction; - dispatch(updateTxDataAndCalculate(txData)); - + dispatch(updateTxDataAndCalculate(transaction)); const { txParams } = transaction; if (txParams.data) { - const { data } = txParams; + const { to: tokenAddress, data } = txParams; const tokenData = getTokenData(data); + const tokens = getTokens(state); + const currentToken = tokens?.find( + ({ address }) => tokenAddress === address, + ); + + dispatch( + updateTokenProps({ + decimals: currentToken?.decimals, + symbol: currentToken?.symbol, + }), + ); dispatch(updateTokenData(tokenData)); } diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js index 8df65c35a..f4ef16303 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.test.js @@ -12,6 +12,7 @@ import ConfirmTransactionReducer, * as actions from './confirm-transaction.duck' const initialState = { txData: {}, tokenData: {}, + tokenProps: {}, fiatTransactionAmount: '', fiatTransactionFee: '', fiatTransactionTotal: '', @@ -307,8 +308,8 @@ describe('Confirm Transaction Duck', () => { nonce: '', tokenData: {}, tokenProps: { - tokenDecimals: '', - tokenSymbol: '', + decimals: '', + symbol: '', }, txData: { ...txData, diff --git a/ui/ducks/gas/gas-action-constants.js b/ui/ducks/gas/gas-action-constants.js index 19cb16ee7..18d1d8dc8 100644 --- a/ui/ducks/gas/gas-action-constants.js +++ b/ui/ducks/gas/gas-action-constants.js @@ -4,11 +4,6 @@ // untangling is having the constants separate. // Actions -export const BASIC_GAS_ESTIMATE_STATUS = - 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS'; export const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA'; -export const SET_BASIC_GAS_ESTIMATE_DATA = - 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA'; export const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'; export const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'; -export const SET_ESTIMATE_SOURCE = 'metamask/gas/SET_ESTIMATE_SOURCE'; diff --git a/ui/ducks/gas/gas-duck.test.js b/ui/ducks/gas/gas-duck.test.js index 221e4dbd8..f5998012a 100644 --- a/ui/ducks/gas/gas-duck.test.js +++ b/ui/ducks/gas/gas-duck.test.js @@ -1,36 +1,14 @@ -import nock from 'nock'; import sinon from 'sinon'; -import BN from 'bn.js'; -import GasReducer, { - setBasicEstimateStatus, - setBasicGasEstimateData, - setCustomGasPrice, - setCustomGasLimit, - fetchBasicGasEstimates, -} from './gas.duck'; +import GasReducer, { setCustomGasPrice, setCustomGasLimit } from './gas.duck'; import { - BASIC_GAS_ESTIMATE_STATUS, - SET_BASIC_GAS_ESTIMATE_DATA, SET_CUSTOM_GAS_PRICE, SET_CUSTOM_GAS_LIMIT, - SET_ESTIMATE_SOURCE, } from './gas-action-constants'; -jest.mock('../../helpers/utils/storage-helpers.js', () => ({ - getStorageItem: jest.fn(), - setStorageItem: jest.fn(), -})); - describe('Gas Duck', () => { let tempDateNow; - const mockGasPriceApiResponse = { - SafeGasPrice: 10, - ProposeGasPrice: 20, - FastGasPrice: 30, - }; - beforeEach(() => { tempDateNow = global.Date.now; @@ -51,22 +29,6 @@ describe('Gas Duck', () => { price: null, limit: null, }, - basicEstimates: { - average: null, - fast: null, - safeLow: null, - }, - basicEstimateStatus: 'LOADING', - estimateSource: '', - }; - - const providerState = { - chainId: '0x1', - nickname: '', - rpcPrefs: {}, - rpcUrl: '', - ticker: 'ETH', - type: 'mainnet', }; describe('GasReducer()', () => { @@ -83,27 +45,6 @@ describe('Gas Duck', () => { ).toStrictEqual(mockState); }); - it('should set basicEstimateStatus to LOADING when receiving a BASIC_GAS_ESTIMATE_STATUS action with value LOADING', () => { - expect( - GasReducer(mockState, { - type: BASIC_GAS_ESTIMATE_STATUS, - value: 'LOADING', - }), - ).toStrictEqual({ basicEstimateStatus: 'LOADING', ...mockState }); - }); - - it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => { - expect( - GasReducer(mockState, { - type: SET_BASIC_GAS_ESTIMATE_DATA, - value: { someProp: 'someData123' }, - }), - ).toStrictEqual({ - basicEstimates: { someProp: 'someData123' }, - ...mockState, - }); - }); - it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => { expect( GasReducer(mockState, { @@ -123,100 +64,6 @@ describe('Gas Duck', () => { }); }); - it('should set estimateSource to Metaswaps when receiving a SET_ESTIMATE_SOURCE action with value Metaswaps', () => { - expect( - GasReducer(mockState, { type: SET_ESTIMATE_SOURCE, value: 'Metaswaps' }), - ).toStrictEqual({ estimateSource: 'Metaswaps', ...mockState }); - }); - - describe('basicEstimateStatus', () => { - it('should create the correct action', () => { - expect(setBasicEstimateStatus('LOADING')).toStrictEqual({ - type: BASIC_GAS_ESTIMATE_STATUS, - value: 'LOADING', - }); - }); - }); - - describe('fetchBasicGasEstimates', () => { - it('should call fetch with the expected params', async () => { - const mockDistpatch = sinon.spy(); - const windowFetchSpy = sinon.spy(window, 'fetch'); - - nock('https://api.metaswap.codefi.network') - .get('/gasPrices') - .reply(200, mockGasPriceApiResponse); - - await fetchBasicGasEstimates()(mockDistpatch, () => ({ - gas: { ...initState }, - metamask: { provider: { ...providerState } }, - })); - - expect(mockDistpatch.getCall(0).args).toStrictEqual([ - { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' }, - ]); - - expect( - windowFetchSpy - .getCall(0) - .args[0].startsWith('https://api.metaswap.codefi.network/gasPrices'), - ).toStrictEqual(true); - - expect(mockDistpatch.getCall(2).args).toStrictEqual([ - { type: 'metamask/gas/SET_ESTIMATE_SOURCE', value: 'MetaSwaps' }, - ]); - - expect(mockDistpatch.getCall(4).args).toStrictEqual([ - { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' }, - ]); - }); - - it('should call fetch with the expected params for test network', async () => { - global.eth = { gasPrice: sinon.fake.returns(new BN(48199313, 10)) }; - - const mockDistpatch = sinon.spy(); - const providerStateForTestNetwork = { - chainId: '0x5', - nickname: '', - rpcPrefs: {}, - rpcUrl: '', - ticker: 'ETH', - type: 'goerli', - }; - - await fetchBasicGasEstimates()(mockDistpatch, () => ({ - gas: { ...initState, basicPriceAEstimatesLastRetrieved: 1000000 }, - metamask: { provider: { ...providerStateForTestNetwork } }, - })); - expect(mockDistpatch.getCall(0).args).toStrictEqual([ - { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'LOADING' }, - ]); - expect(mockDistpatch.getCall(1).args).toStrictEqual([ - { type: 'metamask/gas/SET_ESTIMATE_SOURCE', value: 'eth_gasprice' }, - ]); - expect(mockDistpatch.getCall(2).args).toStrictEqual([ - { - type: SET_BASIC_GAS_ESTIMATE_DATA, - value: { - average: 0.0482, - }, - }, - ]); - expect(mockDistpatch.getCall(3).args).toStrictEqual([ - { type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', value: 'READY' }, - ]); - }); - }); - - describe('setBasicGasEstimateData', () => { - it('should create the correct action', () => { - expect(setBasicGasEstimateData('mockBasicEstimatData')).toStrictEqual({ - type: SET_BASIC_GAS_ESTIMATE_DATA, - value: 'mockBasicEstimatData', - }); - }); - }); - describe('setCustomGasPrice', () => { it('should create the correct action', () => { expect(setCustomGasPrice('mockCustomGasPrice')).toStrictEqual({ diff --git a/ui/ducks/gas/gas.duck.js b/ui/ducks/gas/gas.duck.js index a41c313c5..ea045a2c4 100644 --- a/ui/ducks/gas/gas.duck.js +++ b/ui/ducks/gas/gas.duck.js @@ -1,62 +1,20 @@ import { cloneDeep } from 'lodash'; -import BigNumber from 'bignumber.js'; import { - getStorageItem, - setStorageItem, -} from '../../helpers/utils/storage-helpers'; -import { - decGWEIToHexWEI, - getValueFromWeiHex, -} from '../../helpers/utils/conversions.util'; -import { getIsMainnet, getCurrentChainId } from '../../selectors'; -import fetchWithCache from '../../helpers/utils/fetch-with-cache'; -import { - BASIC_GAS_ESTIMATE_STATUS, RESET_CUSTOM_DATA, - SET_BASIC_GAS_ESTIMATE_DATA, SET_CUSTOM_GAS_LIMIT, SET_CUSTOM_GAS_PRICE, - SET_ESTIMATE_SOURCE, } from './gas-action-constants'; -export const BASIC_ESTIMATE_STATES = { - LOADING: 'LOADING', - FAILED: 'FAILED', - READY: 'READY', -}; - -export const GAS_SOURCE = { - METASWAPS: 'MetaSwaps', - ETHGASPRICE: 'eth_gasprice', -}; - const initState = { customData: { price: null, limit: null, }, - basicEstimates: { - safeLow: null, - average: null, - fast: null, - }, - basicEstimateStatus: BASIC_ESTIMATE_STATES.LOADING, - estimateSource: '', }; // Reducer export default function reducer(state = initState, action) { switch (action.type) { - case BASIC_GAS_ESTIMATE_STATUS: - return { - ...state, - basicEstimateStatus: action.value, - }; - case SET_BASIC_GAS_ESTIMATE_DATA: - return { - ...state, - basicEstimates: action.value, - }; case SET_CUSTOM_GAS_PRICE: return { ...state, @@ -78,138 +36,11 @@ export default function reducer(state = initState, action) { ...state, customData: cloneDeep(initState.customData), }; - case SET_ESTIMATE_SOURCE: - return { - ...state, - estimateSource: action.value, - }; default: return state; } } -// Action Creators -export function setBasicEstimateStatus(status) { - return { - type: BASIC_GAS_ESTIMATE_STATUS, - value: status, - }; -} - -async function basicGasPriceQuery() { - const url = `https://api.metaswap.codefi.network/gasPrices`; - return await fetchWithCache( - url, - { - referrer: url, - referrerPolicy: 'no-referrer-when-downgrade', - method: 'GET', - mode: 'cors', - }, - { cacheRefreshTime: 75000 }, - ); -} - -export function fetchBasicGasEstimates() { - return async (dispatch, getState) => { - const isMainnet = getIsMainnet(getState()); - - dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.LOADING)); - let basicEstimates; - try { - dispatch(setEstimateSource(GAS_SOURCE.ETHGASPRICE)); - if (isMainnet || process.env.IN_TEST) { - try { - basicEstimates = await fetchExternalBasicGasEstimates(); - dispatch(setEstimateSource(GAS_SOURCE.METASWAPS)); - } catch (error) { - basicEstimates = await fetchEthGasPriceEstimates(getState()); - } - } else { - basicEstimates = await fetchEthGasPriceEstimates(getState()); - } - dispatch(setBasicGasEstimateData(basicEstimates)); - dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.READY)); - } catch (error) { - dispatch(setBasicEstimateStatus(BASIC_ESTIMATE_STATES.FAILED)); - } - - return basicEstimates; - }; -} - -async function fetchExternalBasicGasEstimates() { - const { - SafeGasPrice, - ProposeGasPrice, - FastGasPrice, - } = await basicGasPriceQuery(); - - const [safeLow, average, fast] = [ - SafeGasPrice, - ProposeGasPrice, - FastGasPrice, - ].map((price) => new BigNumber(price, 10).toNumber()); - - const basicEstimates = { - safeLow, - average, - fast, - }; - - return basicEstimates; -} - -async function fetchEthGasPriceEstimates(state) { - const chainId = getCurrentChainId(state); - const [cachedTimeLastRetrieved, cachedBasicEstimates] = await Promise.all([ - getStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED`), - getStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES`), - ]); - const timeLastRetrieved = cachedTimeLastRetrieved || 0; - if (cachedBasicEstimates && Date.now() - timeLastRetrieved < 75000) { - return cachedBasicEstimates; - } - const gasPrice = await global.eth.gasPrice(); - const averageGasPriceInDecGWEI = getValueFromWeiHex({ - value: gasPrice.toString(16), - numberOfDecimals: 4, - toDenomination: 'GWEI', - }); - const basicEstimates = { - average: Number(averageGasPriceInDecGWEI), - }; - const timeRetrieved = Date.now(); - - await Promise.all([ - setStorageItem(`${chainId}_BASIC_PRICE_ESTIMATES`, basicEstimates), - setStorageItem( - `${chainId}_BASIC_PRICE_ESTIMATES_LAST_RETRIEVED`, - timeRetrieved, - ), - ]); - - return basicEstimates; -} - -export function setCustomGasPriceForRetry(newPrice) { - return async (dispatch) => { - if (newPrice === '0x0') { - const { fast } = await fetchExternalBasicGasEstimates(); - dispatch(setCustomGasPrice(decGWEIToHexWEI(fast))); - } else { - dispatch(setCustomGasPrice(newPrice)); - } - }; -} - -export function setBasicGasEstimateData(basicGasEstimateData) { - return { - type: SET_BASIC_GAS_ESTIMATE_DATA, - value: basicGasEstimateData, - }; -} - export function setCustomGasPrice(newPrice) { return { type: SET_CUSTOM_GAS_PRICE, @@ -227,10 +58,3 @@ export function setCustomGasLimit(newLimit) { export function resetCustomData() { return { type: RESET_CUSTOM_DATA }; } - -export function setEstimateSource(estimateSource) { - return { - type: SET_ESTIMATE_SOURCE, - value: estimateSource, - }; -} diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 0047b6904..5663c2029 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -1,3 +1,4 @@ +import { addHexPrefix, isHexString } from 'ethereumjs-util'; import * as actionConstants from '../../store/actionConstants'; import { ALERT_TYPES } from '../../../shared/constants/alerts'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; @@ -5,6 +6,9 @@ import { accountsWithSendEtherInfoSelector, getAddressBook, } from '../../selectors'; +import { updateTransaction } from '../../store/actions'; +import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; +import { decGWEIToHexWEI } from '../../helpers/utils/conversions.util'; export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -198,6 +202,47 @@ export default function reduceMetamask(state = {}, action) { } } +const toHexWei = (value, expectHexWei) => { + return addHexPrefix(expectHexWei ? value : decGWEIToHexWEI(value)); +}; + +// Action Creators +export function updateTransactionGasFees({ + gasPrice, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + transaction, + expectHexWei = false, +}) { + return async (dispatch) => { + const txParamsCopy = { ...transaction.txParams, gas: gasLimit }; + if (gasPrice) { + dispatch( + setCustomGasPrice(toHexWei(txParamsCopy.gasPrice, expectHexWei)), + ); + txParamsCopy.gasPrice = toHexWei(gasPrice, expectHexWei); + } else if (maxFeePerGas && maxPriorityFeePerGas) { + txParamsCopy.maxFeePerGas = toHexWei(maxFeePerGas, expectHexWei); + txParamsCopy.maxPriorityFeePerGas = addHexPrefix( + decGWEIToHexWEI(maxPriorityFeePerGas), + ); + } + const updatedTx = { + ...transaction, + txParams: txParamsCopy, + }; + + const customGasLimit = isHexString(addHexPrefix(gasLimit)) + ? addHexPrefix(gasLimit) + : addHexPrefix(gasLimit.toString(16)); + dispatch(setCustomGasLimit(customGasLimit)); + await dispatch(updateTransaction(updatedTx)); + }; +} + +// Selectors + export const getCurrentLocale = (state) => state.metamask.currentLocale; export const getAlertEnabledness = (state) => state.metamask.alertEnabledness; @@ -238,3 +283,19 @@ export function getSendToAccounts(state) { export function getUnapprovedTxs(state) { return state.metamask.unapprovedTxs; } + +export function isEIP1559Network(state) { + return state.metamask.networkDetails.EIPS[1559] === true; +} + +export function getGasEstimateType(state) { + return state.metamask.gasEstimateType; +} + +export function getGasFeeEstimates(state) { + return state.metamask.gasFeeEstimates; +} + +export function getEstimatedGasFeeTimeBounds(state) { + return state.metamask.estimatedGasFeeTimeBounds; +} diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 30ddb2d80..d4013de84 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -9,8 +9,8 @@ import { conversionUtil, multiplyCurrencies, subtractCurrencies, -} from '../../helpers/utils/conversion-util'; -import { GAS_LIMITS } from '../../../shared/constants/gas'; +} from '../../../shared/modules/conversion.utils'; +import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas'; import { CONTRACT_ADDRESS_ERROR, INSUFFICIENT_FUNDS_ERROR, @@ -40,28 +40,23 @@ import { getIsNonStandardEthChain, } from '../../selectors'; import { + disconnectGasFeeEstimatePoller, displayWarning, estimateGas, + getGasFeeEstimatesAndStartPolling, hideLoadingIndication, showConfTxPage, showLoadingIndication, updateTokenType, updateTransaction, } from '../../store/actions'; -import { - fetchBasicGasEstimates, - setCustomGasLimit, - BASIC_ESTIMATE_STATES, -} from '../gas/gas.duck'; -import { - SET_BASIC_GAS_ESTIMATE_DATA, - BASIC_GAS_ESTIMATE_STATUS, -} from '../gas/gas-action-constants'; +import { setCustomGasLimit } from '../gas/gas.duck'; import { QR_CODE_DETECTED, SELECTED_ACCOUNT_CHANGED, ACCOUNT_CHANGED, ADDRESS_BOOK_UPDATED, + GAS_FEE_ESTIMATES_UPDATED, } from '../../store/actionConstants'; import { calcTokenAmount, @@ -74,13 +69,18 @@ import { isOriginContractAddress, isValidDomainName, } from '../../helpers/utils/util'; -import { getTokens, getUnapprovedTxs } from '../metamask/metamask'; +import { + getGasEstimateType, + getTokens, + getUnapprovedTxs, +} from '../metamask/metamask'; import { resetEnsResolution } from '../ens'; import { isBurnAddress, isValidHexAddress, } from '../../../shared/modules/hexstring-utils'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; +import { ETH, GWEI } from '../../helpers/constants/common'; // typedefs /** @@ -350,7 +350,7 @@ export const computeEstimatedGasLimit = createAsyncThunk( if (send.stage !== SEND_STAGES.EDIT) { const gasLimit = await estimateGasLimitForSend({ gasPrice: send.gas.gasPrice, - blockGasLimit: metamask.blockGasLimit, + blockGasLimit: metamask.currentBlockGasLimit, selectedAddress: metamask.selectedAddress, sendToken: send.asset.details, to: send.recipient.address?.toLowerCase(), @@ -368,6 +368,29 @@ export const computeEstimatedGasLimit = createAsyncThunk( }, ); +/** + * This method is used to keep the original logic from the gas.duck.js file + * after receiving a gasPrice from eth_gasPrice. First, the returned gasPrice + * was converted to GWEI, then it was converted to a Number, then in the send + * duck (here) we would use getGasPriceInHexWei to get back to hexWei. Now that + * we receive a GWEI estimate from the controller, we still need to do this + * weird conversion to get the proper rounding. + * @param {T} gasPriceEstimate + * @returns + */ +function getRoundedGasPrice(gasPriceEstimate) { + const gasPriceInDecGwei = conversionUtil(gasPriceEstimate, { + numberOfDecimals: 4, + toDenomination: GWEI, + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromCurrency: ETH, + fromDenomination: GWEI, + }); + const gasPriceAsNumber = Number(gasPriceInDecGwei); + return getGasPriceInHexWei(gasPriceAsNumber); +} + /** * Responsible for initializing required state for the send slice. * This method is dispatched from the send page in the componentDidMount @@ -400,32 +423,42 @@ export const initializeSendState = createAsyncThunk( // the getMetaMaskAccounts selector. getTargetAccount consumes this // selector and returns the account at the specified address. const account = getTargetAccount(state, fromAddress); - // Initiate gas slices work to fetch gasPrice estimates. We need to get the - // new state after this is set to determine if initialization can proceed. - await thunkApi.dispatch(fetchBasicGasEstimates()); + + // Default gasPrice to 1 gwei if all estimation fails + let gasPrice = '0x1'; + let gasEstimatePollToken = null; + + // Instruct the background process that polling for gas prices should begin + gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling(); const { - gas: { basicEstimateStatus, basicEstimates }, + metamask: { gasFeeEstimates, gasEstimateType }, } = thunkApi.getState(); - // Default gasPrice to 1 gwei if all estimation fails - const gasPrice = - basicEstimateStatus === BASIC_ESTIMATE_STATES.READY - ? getGasPriceInHexWei(basicEstimates.average) - : '0x1'; + + if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { + gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { + gasPrice = getRoundedGasPrice(gasFeeEstimates.gasPrice); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { + gasPrice = getGasPriceInHexWei( + gasFeeEstimates.medium.suggestedMaxFeePerGas, + ); + } + // Set a basic gasLimit in the event that other estimation fails let gasLimit = asset.type === ASSET_TYPES.TOKEN ? GAS_LIMITS.BASE_TOKEN_ESTIMATE : GAS_LIMITS.SIMPLE; if ( - basicEstimateStatus === BASIC_ESTIMATE_STATES.READY && + gasEstimateType !== GAS_ESTIMATE_TYPES.NONE && stage !== SEND_STAGES.EDIT && recipient.address ) { // Run our estimateGasLimit logic to get a more accurate estimation of // required gas. If this value isn't nullish, set it as the new gasLimit const estimatedGasLimit = await estimateGasLimitForSend({ - gasPrice: getGasPriceInHexWei(basicEstimates.average), - blockGasLimit: metamask.blockGasLimit, + gasPrice, + blockGasLimit: metamask.currentBlockGasLimit, selectedAddress: fromAddress, sendToken: asset.details, to: recipient.address.toLowerCase(), @@ -463,6 +496,7 @@ export const initializeSendState = createAsyncThunk( gasPrice, gasLimit, gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)), + gasEstimatePollToken, }; }, ); @@ -482,13 +516,18 @@ export const initialState = { gas: { // indicate whether the gas estimate is loading isGasEstimateLoading: true, + // String token indentifying a listener for polling on the gasFeeController + gasEstimatePollToken: null, // has the user set custom gas in the custom gas modal isCustomGasSet: false, // maximum gas needed for tx gasLimit: '0x0', - // price in gwei to pay per gas + // price in wei to pay per gas gasPrice: '0x0', - // maximum total price in gwei to pay + // expected price in wei necessary to pay per gas used for a transaction + // to be included in a reasonable timeframe. Comes from GasFeeController. + gasPriceEstimate: '0x0', + // maximum total price in wei to pay gasTotal: '0x0', // minimum supported gasLimit minimumGasLimit: GAS_LIMITS.SIMPLE, @@ -739,7 +778,9 @@ const slice = createSlice({ // We keep a copy of txParams in state that could be submitted to the // network if the form state is valid. if (state.status === SEND_STATUSES.VALID) { - state.draftTransaction.txParams.from = state.account.address; + if (state.stage !== SEND_STAGES.EDIT) { + state.draftTransaction.txParams.from = state.account.address; + } switch (state.asset.type) { case ASSET_TYPES.TOKEN: // When sending a token the to address is the contract address of @@ -817,6 +858,7 @@ const slice = createSlice({ if ( isSendingToken && + isValidHexAddress(recipient.userInput) && (toChecksumAddress(recipient.userInput) in contractMap || checkExistingAddresses(recipient.userInput, tokens)) ) { @@ -904,7 +946,7 @@ const slice = createSlice({ break; case state.asset.type === ASSET_TYPES.TOKEN && state.asset.details.isERC721 === true: - state.state = SEND_STATUSES.INVALID; + state.status = SEND_STATUSES.INVALID; break; default: state.status = SEND_STATUSES.VALID; @@ -1003,6 +1045,10 @@ const slice = createSlice({ state.gas.gasLimit = action.payload.gasLimit; state.gas.gasPrice = action.payload.gasPrice; state.gas.gasTotal = action.payload.gasTotal; + state.gas.gasEstimatePollToken = action.payload.gasEstimatePollToken; + if (action.payload.gasEstimatePollToken) { + state.gas.isGasEstimateLoading = false; + } if (state.stage !== SEND_STAGES.UNINITIALIZED) { slice.caseReducers.validateRecipientUserInput(state, { payload: { @@ -1042,34 +1088,36 @@ const slice = createSlice({ // because it is no longer loading state.gas.isGasEstimateLoading = false; }) - .addCase(SET_BASIC_GAS_ESTIMATE_DATA, (state, action) => { - // When we receive a new gasPrice via the gas duck we need to update - // the gasPrice in our slice. We call into the caseReducer - // updateGasPrice to also tap into the appropriate follow up checks - // and gasTotal calculation. - slice.caseReducers.updateGasPrice(state, { - payload: getGasPriceInHexWei(action.value.average), - }); - }) - .addCase(BASIC_GAS_ESTIMATE_STATUS, (state, action) => { - // When we fetch gas prices we should temporarily set the form invalid - // Once the price updates we get that value in the - // SET_BASIC_GAS_ESTIMATE_DATA extraReducer above. Finally as long as - // the state is 'READY' we will revalidate the form. - switch (action.value) { - case BASIC_ESTIMATE_STATES.FAILED: - state.status = SEND_STATUSES.INVALID; - state.gas.isGasEstimateLoading = true; - break; - case BASIC_ESTIMATE_STATES.LOADING: - state.status = SEND_STATUSES.INVALID; - state.gas.isGasEstimateLoading = true; - break; - case BASIC_ESTIMATE_STATES.READY: - default: - state.gas.isGasEstimateLoading = false; - slice.caseReducers.validateSendState(state); + .addCase(GAS_FEE_ESTIMATES_UPDATED, (state, action) => { + // When the gasFeeController updates its gas fee estimates we need to + // update and validate state based on those new values + const { gasFeeEstimates, gasEstimateType } = action.payload; + let payload = null; + if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { + payload = getGasPriceInHexWei( + gasFeeEstimates.medium.suggestedMaxFeePerGas, + ); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { + payload = getGasPriceInHexWei(gasFeeEstimates.medium); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { + payload = getRoundedGasPrice(gasFeeEstimates.gasPrice); + } + // If a new gasPrice can be derived, and either the gasPriceEstimate + // was '0x0' or the gasPrice selected matches the previous estimate, + // update the gasPrice. This will ensure that we only update the + // gasPrice if the user is using our previous estimated value. + if ( + payload && + (state.gas.gasPriceEstimate === '0x0' || + state.gas.gasPrice === state.gas.gasPriceEstimate) + ) { + slice.caseReducers.updateGasPrice(state, { + payload, + }); } + + // Record the latest gasPriceEstimate for future comparisons + state.gas.gasPriceEstimate = payload ?? state.gas.gasPriceEstimate; }); }, }); @@ -1083,21 +1131,26 @@ const { useCustomGas, updateGasLimit, updateGasPrice, - resetSendState, validateRecipientUserInput, updateRecipientSearchMode, } = actions; -export { - useDefaultGas, - useCustomGas, - updateGasLimit, - updateGasPrice, - resetSendState, -}; +export { useDefaultGas, useCustomGas, updateGasLimit, updateGasPrice }; // Action Creators +export function resetSendState() { + return async (dispatch, getState) => { + const state = getState(); + dispatch(actions.resetSendState()); + + if (state[name].gas.gasEstimatePollToken) { + await disconnectGasFeeEstimatePoller( + state[name].gas.gasEstimatePollToken, + ); + } + }; +} /** * Updates the amount the user intends to send and performs side effects. * 1. If the current mode is MAX change to INPUT @@ -1448,6 +1501,7 @@ export function getMinimumGasLimitForSend(state) { export function getGasInputMode(state) { const isMainnet = getIsMainnet(state); + const gasEstimateType = getGasEstimateType(state); const showAdvancedGasFields = getAdvancedInlineGasShown(state); if (state[name].gas.isCustomGasSet) { return GAS_INPUT_MODES.CUSTOM; @@ -1455,6 +1509,16 @@ export function getGasInputMode(state) { if ((!isMainnet && !process.env.IN_TEST) || showAdvancedGasFields) { return GAS_INPUT_MODES.INLINE; } + + // We get eth_gasPrice estimation if the legacy API fails but we need to + // instruct the UI to render the INLINE inputs in this case, only on + // mainnet or IN_TEST. + if ( + (isMainnet || process.env.IN_TEST) && + gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE + ) { + return GAS_INPUT_MODES.INLINE; + } return GAS_INPUT_MODES.BASIC; } diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 998e3306f..189251b8e 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -10,9 +10,8 @@ import { KNOWN_RECIPIENT_ADDRESS_WARNING, NEGATIVE_ETH_ERROR, } from '../../pages/send/send.constants'; -import { BASIC_ESTIMATE_STATES } from '../gas/gas.duck'; import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network'; -import { GAS_LIMITS } from '../../../shared/constants/gas'; +import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas'; import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; import sendReducer, { initialState, @@ -42,6 +41,7 @@ jest.mock('../../store/actions', () => { return { ...actual, estimateGas: jest.fn(() => Promise.resolve('0x0')), + getGasFeeEstimatesAndStartPolling: jest.fn(() => Promise.resolve()), updateTokenType: jest.fn(() => Promise.resolve({ isERC721: false })), }; }); @@ -952,6 +952,11 @@ describe('Send Slice', () => { it('should dispatch async action thunk first with pending, then finally fulfilling from minimal state', async () => { getState = jest.fn().mockReturnValue({ metamask: { + gasEstimateType: GAS_ESTIMATE_TYPES.NONE, + gasFeeEstimates: {}, + networkDetails: { + EIPS: {}, + }, accounts: { '0xAddress': { address: '0xAddress', @@ -969,6 +974,7 @@ describe('Send Slice', () => { }, }, send: initialState, + gas: { basicEstimateStatus: 'LOADING', basicEstimatesStatus: { @@ -982,12 +988,12 @@ describe('Send Slice', () => { const action = initializeSendState(); await action(dispatchSpy, getState, undefined); - expect(dispatchSpy).toHaveBeenCalledTimes(4); + expect(dispatchSpy).toHaveBeenCalledTimes(3); expect(dispatchSpy.mock.calls[0][0].type).toStrictEqual( 'send/initializeSendState/pending', ); - expect(dispatchSpy.mock.calls[3][0].type).toStrictEqual( + expect(dispatchSpy.mock.calls[2][0].type).toStrictEqual( 'send/initializeSendState/fulfilled', ); }); @@ -999,6 +1005,7 @@ describe('Send Slice', () => { ...initialState, gas: { gasPrice: '0x0', + gasPriceEstimate: '0x0', gasLimit: '0x5208', gasTotal: '0x0', minimumGasLimit: '0x5208', @@ -1006,9 +1013,12 @@ describe('Send Slice', () => { }; const action = { - type: 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA', - value: { - average: '1', + type: 'GAS_FEE_ESTIMATES_UPDATED', + payload: { + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + medium: '1', + }, }, }; @@ -1019,40 +1029,6 @@ describe('Send Slice', () => { expect(result.gas.gasTotal).toStrictEqual('0x1319718a5000'); }); }); - - describe('BASIC_GAS_ESTIMATE_STATUS', () => { - it('should invalidate the send status when status is LOADING', () => { - const validSendStatusState = { - ...initialState, - status: SEND_STATUSES.VALID, - }; - - const action = { - type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', - value: BASIC_ESTIMATE_STATES.LOADING, - }; - - const result = sendReducer(validSendStatusState, action); - - expect(result.status).not.toStrictEqual(validSendStatusState.status); - }); - - it('should invalidate the send status when status is FAILED and use INLINE gas input mode', () => { - const validSendStatusState = { - ...initialState, - status: SEND_STATUSES.VALID, - }; - - const action = { - type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS', - value: BASIC_ESTIMATE_STATES.FAILED, - }; - - const result = sendReducer(validSendStatusState, action); - - expect(result.status).not.toStrictEqual(validSendStatusState.status); - }); - }); }); describe('Action Creators', () => { diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 376be522a..c047d4134 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -46,7 +46,7 @@ import { decGWEIToHexWEI, hexWEIToDecGWEI, } from '../../helpers/utils/conversions.util'; -import { conversionLessThan } from '../../helpers/utils/conversion-util'; +import { conversionLessThan } from '../../../shared/modules/conversion.utils'; import { calcTokenAmount } from '../../helpers/utils/token-util'; import { getSelectedAccount, diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js index 070ea2748..38e85f779 100644 --- a/ui/helpers/constants/design-system.js +++ b/ui/helpers/constants/design-system.js @@ -139,6 +139,7 @@ export const TEXT_ALIGN = { CENTER: 'center', RIGHT: 'right', JUSTIFY: 'justify', + END: 'end', }; export const FONT_WEIGHT = { diff --git a/ui/helpers/constants/gas.js b/ui/helpers/constants/gas.js new file mode 100644 index 000000000..d9bb1fbd5 --- /dev/null +++ b/ui/helpers/constants/gas.js @@ -0,0 +1,27 @@ +export const GAS_FORM_ERRORS = { + GAS_LIMIT_OUT_OF_BOUNDS: 'editGasLimitOutOfBounds', + MAX_PRIORITY_FEE_TOO_LOW: 'editGasMaxPriorityFeeLow', + MAX_FEE_TOO_LOW: 'editGasMaxFeeLow', + MAX_PRIORITY_FEE_ZERO: 'editGasMaxPriorityFeeZeroError', + MAX_PRIORITY_FEE_HIGH_WARNING: 'editGasMaxPriorityFeeHigh', + MAX_FEE_HIGH_WARNING: 'editGasMaxFeeHigh', +}; + +export function getGasFormErrorText(type, t) { + switch (type) { + case GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS: + return t('editGasLimitOutOfBounds'); + case GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW: + return t('editGasMaxPriorityFeeLow'); + case GAS_FORM_ERRORS.MAX_FEE_TOO_LOW: + return t('editGasMaxFeeLow'); + case GAS_FORM_ERRORS.MAX_PRIORITY_FEE_ZERO: + return t('editGasMaxPriorityFeeZeroError'); + case GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING: + return t('editGasMaxPriorityFeeHigh'); + case GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING: + return t('editGasMaxFeeHigh'); + default: + return ''; + } +} diff --git a/ui/helpers/utils/confirm-tx.util.js b/ui/helpers/utils/confirm-tx.util.js index 5ae5d13c3..42f417c1e 100644 --- a/ui/helpers/utils/confirm-tx.util.js +++ b/ui/helpers/utils/confirm-tx.util.js @@ -9,7 +9,7 @@ import { addCurrencies, multiplyCurrencies, conversionGreaterThan, -} from './conversion-util'; +} from '../../../shared/modules/conversion.utils'; export function increaseLastGasPrice(lastGasPrice) { return addHexPrefix( diff --git a/ui/helpers/utils/conversions.util.js b/ui/helpers/utils/conversions.util.js index b7c901d97..7f191f9fe 100644 --- a/ui/helpers/utils/conversions.util.js +++ b/ui/helpers/utils/conversions.util.js @@ -4,7 +4,7 @@ import { conversionUtil, addCurrencies, subtractCurrencies, -} from './conversion-util'; +} from '../../../shared/modules/conversion.utils'; import { formatCurrency } from './confirm-tx.util'; export function bnToHex(inputBn) { diff --git a/ui/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index 4b6d5dd08..46e3617c8 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -1,8 +1,11 @@ import log from 'loglevel'; import BigNumber from 'bignumber.js'; import contractMap from '@metamask/contract-metadata'; +import { + conversionUtil, + multiplyCurrencies, +} from '../../../shared/modules/conversion.utils'; import * as util from './util'; -import { conversionUtil, multiplyCurrencies } from './conversion-util'; import { formatCurrency } from './confirm-tx.util'; const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { @@ -70,7 +73,7 @@ async function getDecimals(tokenAddress) { const contractMetadataInfo = getContractMetadata(tokenAddress); if (contractMetadataInfo) { - decimals = contractMetadataInfo.decimals; + decimals = contractMetadataInfo.decimals?.toString(); } } diff --git a/ui/helpers/utils/transactions.util.js b/ui/helpers/utils/transactions.util.js index 21a33112a..307e3dffc 100644 --- a/ui/helpers/utils/transactions.util.js +++ b/ui/helpers/utils/transactions.util.js @@ -9,10 +9,9 @@ import { TRANSACTION_GROUP_STATUSES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; +import { addCurrencies } from '../../../shared/modules/conversion.utils'; import fetchWithCache from './fetch-with-cache'; -import { addCurrencies } from './conversion-util'; - const hstInterface = new ethers.utils.Interface(abi); /** diff --git a/ui/hooks/useCancelTransaction.js b/ui/hooks/useCancelTransaction.js index 62cf65d72..9bd19cc87 100644 --- a/ui/hooks/useCancelTransaction.js +++ b/ui/hooks/useCancelTransaction.js @@ -1,21 +1,15 @@ import { useDispatch, useSelector } from 'react-redux'; -import { useCallback } from 'react'; -import { addHexPrefix } from 'ethereumjs-util'; +import { useCallback, useState } from 'react'; import { showModal, showSidebar } from '../store/actions'; import { isBalanceSufficient } from '../pages/send/send.utils'; -import { - getHexGasTotal, - increaseLastGasPrice, -} from '../helpers/utils/confirm-tx.util'; import { getSelectedAccount, getIsMainnet } from '../selectors'; import { getConversionRate } from '../ducks/metamask/metamask'; -import { - setCustomGasLimit, - setCustomGasPriceForRetry, -} from '../ducks/gas/gas.duck'; -import { multiplyCurrencies } from '../helpers/utils/conversion-util'; +import { setCustomGasLimit, setCustomGasPrice } from '../ducks/gas/gas.duck'; import { GAS_LIMITS } from '../../shared/constants/gas'; +import { isLegacyTransaction } from '../../shared/modules/transaction.utils'; +import { getMaximumGasTotalInHexWei } from '../../shared/modules/gas.utils'; +import { useIncrementedGasFees } from './useIncrementedGasFees'; /** * Determine whether a transaction can be cancelled and provide a method to @@ -30,33 +24,37 @@ import { GAS_LIMITS } from '../../shared/constants/gas'; export function useCancelTransaction(transactionGroup) { const { primaryTransaction } = transactionGroup; - const transactionGasPrice = primaryTransaction.txParams?.gasPrice; - const gasPrice = - transactionGasPrice === undefined || transactionGasPrice?.startsWith('-') - ? '0x0' - : primaryTransaction.txParams?.gasPrice; - const transaction = primaryTransaction; + const customGasSettings = useIncrementedGasFees(transactionGroup); + const dispatch = useDispatch(); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); - const defaultNewGasPrice = addHexPrefix( - multiplyCurrencies(gasPrice, 1.1, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 10, - }), - ); const isMainnet = useSelector(getIsMainnet); const hideBasic = !(isMainnet || process.env.IN_TEST); + + const [showCancelEditGasPopover, setShowCancelEditGasPopover] = useState( + false, + ); + + const closeCancelEditGasPopover = () => setShowCancelEditGasPopover(false); + const cancelTransaction = useCallback( (event) => { event.stopPropagation(); - dispatch(setCustomGasLimit(GAS_LIMITS.SIMPLE)); - dispatch(setCustomGasPriceForRetry(defaultNewGasPrice)); + if (process.env.SHOW_EIP_1559_UI) { + return setShowCancelEditGasPopover(true); + } + if (isLegacyTransaction(primaryTransaction)) { + // To support the current process of cancelling or speeding up + // a transaction, we have to inform the custom gas state of the new + // gasPrice/gasLimit to start at. + dispatch(setCustomGasPrice(customGasSettings.gasPrice)); + dispatch(setCustomGasLimit(GAS_LIMITS.SIMPLE)); + } const tx = { - ...transaction, + ...primaryTransaction, txParams: { - ...transaction.txParams, + ...primaryTransaction.txParams, gas: GAS_LIMITS.SIMPLE, value: '0x0', }, @@ -68,18 +66,16 @@ export function useCancelTransaction(transactionGroup) { props: { hideBasic, transaction: tx, - onSubmit: (newGasLimit, newGasPrice) => { - const userCustomizedGasTotal = getHexGasTotal({ - gasPrice: newGasPrice, - gasLimit: newGasLimit, - }); + onSubmit: (newGasSettings) => { + const userCustomizedGasTotal = getMaximumGasTotalInHexWei( + newGasSettings, + ); dispatch( showModal({ name: 'CANCEL_TRANSACTION', newGasFee: userCustomizedGasTotal, - transactionId: transaction.id, - defaultNewGasPrice: newGasPrice, - gasLimit: newGasLimit, + transactionId: primaryTransaction.id, + customGasSettings: newGasSettings, }), ); }, @@ -87,20 +83,20 @@ export function useCancelTransaction(transactionGroup) { }), ); }, - [dispatch, transaction, defaultNewGasPrice, hideBasic], + [dispatch, primaryTransaction, customGasSettings, hideBasic], ); const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ amount: '0x0', - gasTotal: getHexGasTotal({ - gasPrice: increaseLastGasPrice(gasPrice), - gasLimit: primaryTransaction.txParams.gas, - }), + gasTotal: getMaximumGasTotalInHexWei(customGasSettings), balance: selectedAccount.balance, conversionRate, }); - return [hasEnoughCancelGas, cancelTransaction]; + return [ + hasEnoughCancelGas, + { cancelTransaction, showCancelEditGasPopover, closeCancelEditGasPopover }, + ]; } diff --git a/ui/hooks/useCancelTransaction.test.js b/ui/hooks/useCancelTransaction.test.js index 6bc391475..711866bb4 100644 --- a/ui/hooks/useCancelTransaction.test.js +++ b/ui/hooks/useCancelTransaction.test.js @@ -58,8 +58,10 @@ describe('useCancelTransaction', function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); - expect(typeof result.current[1]).toStrictEqual('function'); - result.current[1]({ + expect(typeof result.current[1].cancelTransaction).toStrictEqual( + 'function', + ); + result.current[1].cancelTransaction({ preventDefault: () => undefined, stopPropagation: () => undefined, }); @@ -77,10 +79,10 @@ describe('useCancelTransaction', function () { ).toStrictEqual(transactionId); // call onSubmit myself - dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( - GAS_LIMITS.SIMPLE, - '0x1', - ); + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit({ + gasLimit: GAS_LIMITS.SIMPLE, + gasPrice: '0x1', + }); expect( dispatch.calledWith( @@ -88,8 +90,10 @@ describe('useCancelTransaction', function () { name: 'CANCEL_TRANSACTION', transactionId, newGasFee: GAS_LIMITS.SIMPLE, - defaultNewGasPrice: '0x1', - gasLimit: GAS_LIMITS.SIMPLE, + customGasSettings: { + gasPrice: '0x1', + gasLimit: GAS_LIMITS.SIMPLE, + }, }), ), ).toStrictEqual(true); @@ -132,8 +136,10 @@ describe('useCancelTransaction', function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); - expect(typeof result.current[1]).toStrictEqual('function'); - result.current[1]({ + expect(typeof result.current[1].cancelTransaction).toStrictEqual( + 'function', + ); + result.current[1].cancelTransaction({ preventDefault: () => undefined, stopPropagation: () => undefined, }); @@ -147,10 +153,10 @@ describe('useCancelTransaction', function () { .id, ).toStrictEqual(transactionId); - dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( - GAS_LIMITS.SIMPLE, - '0x1', - ); + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit({ + gasLimit: GAS_LIMITS.SIMPLE, + gasPrice: '0x1', + }); expect( dispatch.calledWith( @@ -158,8 +164,10 @@ describe('useCancelTransaction', function () { name: 'CANCEL_TRANSACTION', transactionId, newGasFee: GAS_LIMITS.SIMPLE, - defaultNewGasPrice: '0x1', - gasLimit: GAS_LIMITS.SIMPLE, + customGasSettings: { + gasPrice: '0x1', + gasLimit: GAS_LIMITS.SIMPLE, + }, }), ), ).toStrictEqual(true); diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index a1b67213e..465cadf9e 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -10,7 +10,7 @@ import { getNativeCurrency, } from '../ducks/metamask/metamask'; -import { conversionUtil } from '../helpers/utils/conversion-util'; +import { conversionUtil } from '../../shared/modules/conversion.utils'; /** * Defines the shape of the options parameter for useCurrencyDisplay diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js new file mode 100644 index 000000000..ec378b134 --- /dev/null +++ b/ui/hooks/useGasFeeEstimates.js @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; +import { + getEstimatedGasFeeTimeBounds, + getGasEstimateType, + getGasFeeEstimates, + isEIP1559Network, +} from '../ducks/metamask/metamask'; +import { + disconnectGasFeeEstimatePoller, + getGasFeeEstimatesAndStartPolling, +} from '../store/actions'; + +/** + * @typedef {keyof typeof GAS_ESTIMATE_TYPES} GasEstimateTypes + */ + +/** + * @typedef {object} GasEstimates + * @property {GasEstimateTypes} gasEstimateType - The type of estimate provided + * @property {import( + * '@metamask/controllers' + * ).GasFeeState['gasFeeEstimates']} gasFeeEstimates - The estimate object + * @property {import( + * '@metamask/controllers' + * ).GasFeeState['estimatedGasFeeTimeBounds']} [estimatedGasFeeTimeBounds] - + * estimated time boundaries for fee-market type estimates + * @property {boolean} isGasEstimateLoading - indicates whether the gas + * estimates are currently loading. + */ + +/** + * Gets the current gasFeeEstimates from state and begins polling for new + * estimates. When this hook is removed from the tree it will signal to the + * GasFeeController that it is done requiring new gas estimates. Also checks + * the returned gas estimate for validity on the current network. + * + * @returns {GasFeeEstimates} - GasFeeEstimates object + */ +export function useGasFeeEstimates() { + const supportsEIP1559 = useSelector(isEIP1559Network); + const gasEstimateType = useSelector(getGasEstimateType); + const gasFeeEstimates = useSelector(getGasFeeEstimates); + const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds); + useEffect(() => { + let active = true; + let pollToken; + getGasFeeEstimatesAndStartPolling().then((newPollToken) => { + if (active) { + pollToken = newPollToken; + } else { + disconnectGasFeeEstimatePoller(newPollToken); + } + }); + return () => { + active = false; + if (pollToken) { + disconnectGasFeeEstimatePoller(pollToken); + } + }; + }, []); + + // We consider the gas estimate to be loading if the gasEstimateType is + // 'NONE' or if the current gasEstimateType does not match the type we expect + // for the current network. e.g, a ETH_GASPRICE estimate when on a network + // supporting EIP-1559. + const isGasEstimatesLoading = + gasEstimateType === GAS_ESTIMATE_TYPES.NONE || + (supportsEIP1559 && gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) || + (!supportsEIP1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET); + + return { + gasFeeEstimates, + gasEstimateType, + estimatedGasFeeTimeBounds, + isGasEstimatesLoading, + }; +} diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js new file mode 100644 index 000000000..d30127759 --- /dev/null +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -0,0 +1,238 @@ +import { cleanup, renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; +import createRandomId from '../../shared/modules/random-id'; +import { + getGasEstimateType, + getGasFeeEstimates, + isEIP1559Network, +} from '../ducks/metamask/metamask'; +import { + disconnectGasFeeEstimatePoller, + getGasFeeEstimatesAndStartPolling, +} from '../store/actions'; +import { useGasFeeEstimates } from './useGasFeeEstimates'; + +jest.mock('../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest.fn(), +})); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + + return { + ...actual, + useSelector: jest.fn(), + }; +}); + +const DEFAULT_OPTS = { + isEIP1559Network: false, + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + low: '10', + medium: '20', + high: '30', + }, +}; + +const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { + if (selector === isEIP1559Network) { + return opts.isEIP1559Network ?? DEFAULT_OPTS.isEIP1559Network; + } + if (selector === getGasEstimateType) { + return opts.gasEstimateType ?? DEFAULT_OPTS.gasEstimateType; + } + if (selector === getGasFeeEstimates) { + return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates; + } + return undefined; +}; + +describe('useGasFeeEstimates', () => { + let tokens = []; + beforeEach(() => { + jest.clearAllMocks(); + tokens = []; + getGasFeeEstimatesAndStartPolling.mockImplementation(() => { + const token = createRandomId(); + tokens.push(token); + return Promise.resolve(token); + }); + disconnectGasFeeEstimatePoller.mockImplementation((token) => { + tokens = tokens.filter((tkn) => tkn !== token); + }); + useSelector.mockImplementation(generateUseSelectorRouter()); + }); + + it('registers with the controller', () => { + renderHook(() => useGasFeeEstimates()); + expect(tokens).toHaveLength(1); + }); + + it('clears token with the controller on unmount', async () => { + renderHook(() => useGasFeeEstimates()); + expect(tokens).toHaveLength(1); + const expectedToken = tokens[0]; + await cleanup(); + expect(getGasFeeEstimatesAndStartPolling).toHaveBeenCalledTimes(1); + expect(disconnectGasFeeEstimatePoller).toHaveBeenCalledWith(expectedToken); + expect(tokens).toHaveLength(0); + }); + + it('works with LEGACY gas prices', () => { + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates: DEFAULT_OPTS.gasFeeEstimates, + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: false, + }); + }); + + it('works with ETH_GASPRICE gas prices', () => { + const gasFeeEstimates = { gasPrice: '10' }; + useSelector.mockImplementation( + generateUseSelectorRouter({ + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + gasFeeEstimates, + }), + ); + + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates, + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: false, + }); + }); + + it('works with FEE_MARKET gas prices', () => { + const gasFeeEstimates = { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', + }; + useSelector.mockImplementation( + generateUseSelectorRouter({ + isEIP1559Network: true, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates, + }), + ); + + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: false, + }); + }); + + it('indicates that gas estimates are loading when gasEstimateType is NONE', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + gasEstimateType: GAS_ESTIMATE_TYPES.NONE, + gasFeeEstimates: {}, + }), + ); + + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates: {}, + gasEstimateType: GAS_ESTIMATE_TYPES.NONE, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: true, + }); + }); + + it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET but network supports EIP-1559', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + isEIP1559Network: true, + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + gasFeeEstimates: { + gasPrice: '10', + }, + }), + ); + + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates: { gasPrice: '10' }, + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: true, + }); + }); + + it('indicates that gas estimates are loading when gasEstimateType is FEE_MARKET but network does not support EIP-1559', () => { + const gasFeeEstimates = { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', + }; + useSelector.mockImplementation( + generateUseSelectorRouter({ + isEIP1559Network: false, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates, + }), + ); + + const { + result: { current }, + } = renderHook(() => useGasFeeEstimates()); + expect(current).toMatchObject({ + gasFeeEstimates, + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + estimatedGasFeeTimeBounds: undefined, + isGasEstimatesLoading: true, + }); + }); +}); diff --git a/ui/hooks/useGasFeeInputs.js b/ui/hooks/useGasFeeInputs.js new file mode 100644 index 000000000..d8707c6ae --- /dev/null +++ b/ui/hooks/useGasFeeInputs.js @@ -0,0 +1,366 @@ +import { addHexPrefix } from 'ethereumjs-util'; +import { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; +import { multiplyCurrencies } from '../../shared/modules/conversion.utils'; +import { + getMaximumGasTotalInHexWei, + getMinimumGasTotalInHexWei, +} from '../../shared/modules/gas.utils'; +import { PRIMARY, SECONDARY } from '../helpers/constants/common'; +import { + decGWEIToHexWEI, + decimalToHex, +} from '../helpers/utils/conversions.util'; +import { getShouldShowFiat } from '../selectors'; +import { GAS_FORM_ERRORS } from '../helpers/constants/gas'; +import { useCurrencyDisplay } from './useCurrencyDisplay'; +import { useGasFeeEstimates } from './useGasFeeEstimates'; +import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; + +const HIGH_FEE_WARNING_MULTIPLIER = 1.5; + +/** + * Opaque string type representing a decimal (base 10) number in GWEI + * @typedef {`${number}`} DecGweiString + */ + +/** + * String value representing the active estimate level to use + * @typedef {'low' | 'medium' | 'high'} EstimateLevel + */ + +/** + * Pulls out gasPrice estimate from either of the two gasPrice estimation + * sources, based on the gasEstimateType and current estimateToUse. + * @param {{import( + * '@metamask/controllers' + * ).GasFeeState['gasFeeEstimates']}} gasFeeEstimates - estimates returned from + * the controller + * @param {import( + * './useGasFeeEstimates' + * ).GasEstimates} gasEstimateType - type of estimate returned from controller + * @param {EstimateLevel} estimateToUse - current estimate level to use + * @returns {[DecGweiString]} - gasPrice estimate to use or null + */ +function getGasPriceEstimate(gasFeeEstimates, gasEstimateType, estimateToUse) { + if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { + return gasFeeEstimates?.[estimateToUse] ?? '0'; + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { + return gasFeeEstimates?.gasPrice ?? '0'; + } + return '0'; +} + +/** + * Pulls out gas fee estimate from the estimates returned from controller, + * based on the gasEstimateType and current estimateToUse. + * @param {'maxFeePerGas' | 'maxPriorityFeePerGas'} field - field to select + * @param {{import( + * '@metamask/controllers' + * ).GasFeeState['gasFeeEstimates']}} gasFeeEstimates - estimates returned from + * the controller + * @param {import( + * './useGasFeeEstimates' + * ).GasEstimates} gasEstimateType - type of estimate returned from controller + * @param {EstimateLevel} estimateToUse - current estimate level to use + * @returns {[DecGweiString]} - gas fee estimate to use or null + */ +function getGasFeeEstimate( + field, + gasFeeEstimates, + gasEstimateType, + estimateToUse, +) { + if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { + return gasFeeEstimates?.[estimateToUse]?.[field] ?? '0'; + } + return '0'; +} + +/** + * @typedef {Object} GasFeeInputReturnType + * @property {DecGweiString} [maxFeePerGas] - the maxFeePerGas input value. + * @property {string} [maxFeePerGasFiat] - the maxFeePerGas converted to the + * user's preferred currency. + * @property {(DecGweiString) => void} setMaxFeePerGas - state setter method to + * update the maxFeePerGas. + * @property {DecGweiString} [maxPriorityFeePerGas] - the maxPriorityFeePerGas + * input value. + * @property {string} [maxPriorityFeePerGasFiat] - the maxPriorityFeePerGas + * converted to the user's preferred currency. + * @property {(DecGweiString) => void} setMaxPriorityFeePerGas - state setter + * method to update the maxPriorityFeePerGas. + * @property {DecGweiString} [gasPrice] - the gasPrice input value. + * @property {(DecGweiString) => void} setGasPrice - state setter method to + * update the gasPrice. + * @property {DecGweiString} gasLimit - the gasLimit input value. + * @property {(DecGweiString) => void} setGasLimit - state setter method to + * update the gasLimit. + * @property {EstimateLevel} [estimateToUse] - the estimate level currently + * selected. This will be null if the user has ejected from using the + * estimates. + * @property {([EstimateLevel]) => void} setEstimateToUse - Setter method for + * choosing which EstimateLevel to use. + * @property {string} [estimatedMinimumFiat] - The amount estimated to be paid + * based on current network conditions. Expressed in user's preferred + * currency. + * @property {string} [estimatedMaximumFiat] - the maximum amount estimated to be + * paid if current network transaction volume increases. Expressed in user's + * preferred currency. + * @property {string} [estimatedMaximumNative] - the maximum amount estimated to + * be paid if the current network transaction volume increases. Expressed in + * the network's native currency. + */ + +/** + * Uses gasFeeEstimates and state to keep track of user gas fee inputs. + * Will update the gas fee state when estimates update if the user has not yet + * modified the fields. + * @param {EstimateLevel} defaultEstimateToUse - which estimate + * level to default the 'estimateToUse' state variable to. + * @returns {GasFeeInputReturnType & import( + * './useGasFeeEstimates' + * ).GasEstimates} - gas fee input state and the GasFeeEstimates object + */ +export function useGasFeeInputs(defaultEstimateToUse = 'medium') { + // We need to know whether to show fiat conversions or not, so that we can + // default our fiat values to empty strings if showing fiat is not wanted or + // possible. + const showFiat = useSelector(getShouldShowFiat); + + // We need to know the current network's currency and its decimal precision + // to calculate the amount to display to the user. + const { + currency: primaryCurrency, + numberOfDecimals: primaryNumberOfDecimals, + } = useUserPreferencedCurrency(PRIMARY); + + // For calculating the value of gas fees in the user's preferred currency we + // first have to know what that currency is and its decimal precision + const { + currency: fiatCurrency, + numberOfDecimals: fiatNumberOfDecimals, + } = useUserPreferencedCurrency(SECONDARY); + + // This hook keeps track of a few pieces of transitional state. It is + // transitional because it is only used to modify a transaction in the + // metamask (background) state tree. + const [maxFeePerGas, setMaxFeePerGas] = useState(null); + const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(null); + const [gasPrice, setGasPrice] = useState(null); + const [gasLimit, setGasLimit] = useState(21000); + const [estimateToUse, setInternalEstimateToUse] = useState( + defaultEstimateToUse, + ); + + // We need the gas estimates from the GasFeeController in the background. + // Calling this hooks initiates polling for new gas estimates and returns the + // current estimate. + const { + gasEstimateType, + gasFeeEstimates, + isGasEstimatesLoading, + estimatedGasFeeTimeBounds, + } = useGasFeeEstimates(); + + // When a user selects an estimate level, it will wipe out what they have + // previously put in the inputs. This returns the inputs to the estimated + // values at the level specified. + const setEstimateToUse = useCallback((estimateLevel) => { + setInternalEstimateToUse(estimateLevel); + setMaxFeePerGas(null); + setMaxPriorityFeePerGas(null); + setGasPrice(null); + }, []); + + // We specify whether to use the estimate value by checking if the state + // value has been set. The state value is only set by user input and is wiped + // when the user selects an estimate. Default here is '0' to avoid bignumber + // errors in later calculations for nullish values. + const maxFeePerGasToUse = + maxFeePerGas ?? + getGasFeeEstimate( + 'suggestedMaxFeePerGas', + gasFeeEstimates, + gasEstimateType, + estimateToUse, + ); + + const maxPriorityFeePerGasToUse = + maxPriorityFeePerGas ?? + getGasFeeEstimate( + 'suggestedMaxPriorityFeePerGas', + gasFeeEstimates, + gasEstimateType, + estimateToUse, + ); + + const gasPriceToUse = + gasPrice ?? + getGasPriceEstimate(gasFeeEstimates, gasEstimateType, estimateToUse); + + // We have two helper methods that take an object that can have either + // gasPrice OR the EIP-1559 fields on it, plus gasLimit. This object is + // conditionally set to the appropriate fields to compute the minimum + // and maximum cost of a transaction given the current estimates or selected + // gas fees. + const gasSettings = { + gasLimit: decimalToHex(gasLimit), + }; + if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { + gasSettings.maxFeePerGas = decGWEIToHexWEI(maxFeePerGasToUse); + gasSettings.maxPriorityFeePerGas = decGWEIToHexWEI( + maxPriorityFeePerGasToUse, + ); + gasSettings.baseFeePerGas = decGWEIToHexWEI( + gasFeeEstimates.estimatedBaseFee ?? '0', + ); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.NONE) { + gasSettings.gasPrice = '0x0'; + } else { + gasSettings.gasPrice = decGWEIToHexWEI(gasPriceToUse); + } + + // The maximum amount this transaction will cost + const maximumCostInHexWei = getMaximumGasTotalInHexWei(gasSettings); + // The minimum amount this transaction will cost's + const minimumCostInHexWei = getMinimumGasTotalInHexWei(gasSettings); + + // We need to display the estimated fiat currency impact of the + // maxPriorityFeePerGas field to the user. This hook calculates that amount. + const [, { value: maxPriorityFeePerGasFiat }] = useCurrencyDisplay( + addHexPrefix( + multiplyCurrencies(maxPriorityFeePerGasToUse, gasLimit, { + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + multiplicandBase: 10, + multiplierBase: 10, + }), + ), + { + numberOfDecimals: fiatNumberOfDecimals, + currency: fiatCurrency, + }, + ); + + // We need to display thee estimated fiat currency impact of the maxFeePerGas + // field to the user. This hook calculates that amount. This also works for + // the gasPrice amount because in legacy transactions cost is always gasPrice + // * gasLimit. + const [, { value: maxFeePerGasFiat }] = useCurrencyDisplay( + maximumCostInHexWei, + { + numberOfDecimals: fiatNumberOfDecimals, + currency: fiatCurrency, + }, + ); + + // We need to display the total amount of native currency will be expended + // given the selected gas fees. + const [estimatedMaximumNative] = useCurrencyDisplay(maximumCostInHexWei, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); + + // We also need to display our closest estimate of the low end of estimation + // in fiat. + const [, { value: estimatedMinimumFiat }] = useCurrencyDisplay( + minimumCostInHexWei, + { + numberOfDecimals: fiatNumberOfDecimals, + currency: fiatCurrency, + }, + ); + + // Separating errors from warnings so we can know which value problems + // are blocking or simply useful information for the users + const gasErrors = {}; + const gasWarnings = {}; + + if (gasLimit < 21000 || gasLimit > 7920027) { + gasErrors.gasLimit = GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS; + } + + switch (gasEstimateType) { + case GAS_ESTIMATE_TYPES.FEE_MARKET: + if (maxPriorityFeePerGasToUse < 1) { + gasErrors.maxPriorityFee = GAS_FORM_ERRORS.MAX_PRIORITY_FEE_ZERO; + } else if ( + !isGasEstimatesLoading && + maxPriorityFeePerGasToUse < + gasFeeEstimates?.low?.suggestedMaxPriorityFeePerGas + ) { + gasErrors.maxPriorityFee = GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW; + } else if ( + gasFeeEstimates?.high && + maxPriorityFeePerGasToUse > + gasFeeEstimates.high.suggestedMaxPriorityFeePerGas * + HIGH_FEE_WARNING_MULTIPLIER + ) { + gasWarnings.maxPriorityFee = + GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING; + } + + if ( + !isGasEstimatesLoading && + maxFeePerGasToUse < gasFeeEstimates?.low?.suggestedMaxFeePerGas + ) { + gasErrors.maxFee = GAS_FORM_ERRORS.MAX_FEE_TOO_LOW; + } else if ( + gasFeeEstimates?.high && + maxFeePerGasToUse > + gasFeeEstimates.high.suggestedMaxFeePerGas * + HIGH_FEE_WARNING_MULTIPLIER + ) { + gasWarnings.maxFee = GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING; + } + break; + default: + break; + } + + // Determine if we have any errors which should block submission + const hasBlockingGasErrors = Boolean(Object.keys(gasErrors).length); + + // Now that we've determined errors that block submission, we can pool the warnings + // and errors into one object for easier use within the UI. This object should have + // no effect on whether or not the user can submit the form + const errorsAndWarnings = { + ...gasErrors, + ...gasWarnings, + }; + + return { + maxFeePerGas: maxFeePerGasToUse, + maxFeePerGasFiat: showFiat ? maxFeePerGasFiat : '', + setMaxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGasToUse, + maxPriorityFeePerGasFiat: showFiat ? maxPriorityFeePerGasFiat : '', + setMaxPriorityFeePerGas, + gasPrice: gasPriceToUse, + setGasPrice, + gasLimit, + setGasLimit, + estimateToUse, + setEstimateToUse, + estimatedMinimumFiat: showFiat ? estimatedMinimumFiat : '', + estimatedMaximumFiat: showFiat ? maxFeePerGasFiat : '', + estimatedMaximumNative, + isGasEstimatesLoading, + gasFeeEstimates, + gasEstimateType, + estimatedGasFeeTimeBounds, + gasErrors: errorsAndWarnings, + hasGasErrors: hasBlockingGasErrors, + onManualChange: () => { + setEstimateToUse(null); + // Restore existing values + setGasPrice(gasPriceToUse); + setGasLimit(gasLimit); + setMaxFeePerGas(maxFeePerGasToUse); + setMaxPriorityFeePerGas(maxPriorityFeePerGasToUse); + }, + }; +} diff --git a/ui/hooks/useGasFeeInputs.test.js b/ui/hooks/useGasFeeInputs.test.js new file mode 100644 index 000000000..a4b1400bb --- /dev/null +++ b/ui/hooks/useGasFeeInputs.test.js @@ -0,0 +1,237 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas'; +import { multiplyCurrencies } from '../../shared/modules/conversion.utils'; +import { + getConversionRate, + getNativeCurrency, +} from '../ducks/metamask/metamask'; +import { ETH, PRIMARY } from '../helpers/constants/common'; +import { getCurrentCurrency, getShouldShowFiat } from '../selectors'; +import { useGasFeeEstimates } from './useGasFeeEstimates'; +import { useGasFeeInputs } from './useGasFeeInputs'; +import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; + +jest.mock('./useUserPreferencedCurrency', () => ({ + useUserPreferencedCurrency: jest.fn(), +})); + +jest.mock('./useGasFeeEstimates', () => ({ + useGasFeeEstimates: jest.fn(), +})); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + + return { + ...actual, + useSelector: jest.fn(), + }; +}); + +// Why this number? +// 20 gwei * 21000 gasLimit = 420,000 gwei +// 420,000 gwei is 0.00042 ETH +// 0.00042 ETH * 100000 = $42 +const MOCK_ETH_USD_CONVERSION_RATE = 100000; + +const LEGACY_GAS_ESTIMATE_RETURN_VALUE = { + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + low: '10', + medium: '20', + high: '30', + }, + estimatedGasFeeTimeBounds: {}, +}; + +const FEE_MARKET_ESTIMATE_RETURN_VALUE = { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: { + minWaitTimeEstimate: 180000, + maxWaitTimeEstimate: 300000, + suggestedMaxPriorityFeePerGas: '3', + suggestedMaxFeePerGas: '53', + }, + medium: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 60000, + suggestedMaxPriorityFeePerGas: '7', + suggestedMaxFeePerGas: '70', + }, + high: { + minWaitTimeEstimate: 0, + maxWaitTimeEstimate: 15000, + suggestedMaxPriorityFeePerGas: '10', + suggestedMaxFeePerGas: '100', + }, + estimatedBaseFee: '50', + }, + estimatedGasFeeTimeBounds: {}, +}; + +const generateUseSelectorRouter = () => (selector) => { + if (selector === getConversionRate) { + return MOCK_ETH_USD_CONVERSION_RATE; + } + if (selector === getNativeCurrency) { + return ETH; + } + if (selector === getCurrentCurrency) { + return 'USD'; + } + if (selector === getShouldShowFiat) { + return true; + } + return undefined; +}; + +function getTotalCostInETH(gwei, gasLimit) { + return multiplyCurrencies(gwei, gasLimit, { + fromDenomination: 'GWEI', + toDenomination: 'ETH', + multiplicandBase: 10, + multiplierBase: 10, + }); +} + +describe('useGasFeeInputs', () => { + beforeEach(() => { + jest.clearAllMocks(); + useUserPreferencedCurrency.mockImplementation((type) => { + if (type === PRIMARY) { + return { currency: ETH, numberOfDecimals: 6 }; + } + return { currency: 'USD', numberOfDecimals: 2 }; + }); + }); + + describe('when using gasPrice API for estimation', () => { + beforeEach(() => { + useGasFeeEstimates.mockImplementation( + () => LEGACY_GAS_ESTIMATE_RETURN_VALUE, + ); + useSelector.mockImplementation(generateUseSelectorRouter()); + }); + it('passes through the raw estimate values from useGasFeeEstimates', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.gasFeeEstimates).toMatchObject( + LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasFeeEstimates, + ); + expect(result.current.gasEstimateType).toBe( + LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasEstimateType, + ); + expect(result.current.estimatedGasFeeTimeBounds).toMatchObject({}); + }); + + it('returns gasPrice appropriately, and "0" for EIP1559 fields', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.gasPrice).toBe( + LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium, + ); + expect(result.current.maxFeePerGas).toBe('0'); + expect(result.current.maxPriorityFeePerGas).toBe('0'); + }); + + it('updates values when user modifies gasPrice', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.gasPrice).toBe( + LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium, + ); + let totalEthGasFee = getTotalCostInETH( + LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium, + result.current.gasLimit, + ); + let totalFiat = ( + Number(totalEthGasFee) * MOCK_ETH_USD_CONVERSION_RATE + ).toFixed(2); + expect(result.current.estimatedMaximumNative).toBe( + `${totalEthGasFee} ETH`, + ); + expect(result.current.estimatedMaximumFiat).toBe(`$${totalFiat}`); + expect(result.current.estimatedMinimumFiat).toBe(`$${totalFiat}`); + act(() => { + result.current.setGasPrice('30'); + }); + totalEthGasFee = getTotalCostInETH('30', result.current.gasLimit); + totalFiat = ( + Number(totalEthGasFee) * MOCK_ETH_USD_CONVERSION_RATE + ).toFixed(2); + expect(result.current.gasPrice).toBe('30'); + expect(result.current.estimatedMaximumNative).toBe( + `${totalEthGasFee} ETH`, + ); + expect(result.current.estimatedMaximumFiat).toBe(`$${totalFiat}`); + expect(result.current.estimatedMinimumFiat).toBe(`$${totalFiat}`); + }); + }); + + describe('when using EIP 1559 API for estimation', () => { + beforeEach(() => { + useGasFeeEstimates.mockImplementation( + () => FEE_MARKET_ESTIMATE_RETURN_VALUE, + ); + useSelector.mockImplementation(generateUseSelectorRouter()); + }); + it('passes through the raw estimate values from useGasFeeEstimates', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.gasFeeEstimates).toMatchObject( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates, + ); + expect(result.current.gasEstimateType).toBe( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasEstimateType, + ); + expect(result.current.estimatedGasFeeTimeBounds).toMatchObject({}); + }); + + it('returns EIP-1559 fields appropriately, and "0" for gasPrice fields', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.gasPrice).toBe('0'); + expect(result.current.maxFeePerGas).toBe( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium + .suggestedMaxFeePerGas, + ); + expect(result.current.maxPriorityFeePerGas).toBe( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium + .suggestedMaxPriorityFeePerGas, + ); + }); + + it('updates values when user modifies maxFeePerGas', () => { + const { result } = renderHook(() => useGasFeeInputs()); + expect(result.current.maxFeePerGas).toBe( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium + .suggestedMaxFeePerGas, + ); + let totalEthGasFee = getTotalCostInETH( + FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium + .suggestedMaxFeePerGas, + result.current.gasLimit, + ); + let totalMaxFiat = ( + Number(totalEthGasFee) * MOCK_ETH_USD_CONVERSION_RATE + ).toFixed(2); + expect(result.current.estimatedMaximumNative).toBe( + `${totalEthGasFee} ETH`, + ); + expect(result.current.estimatedMaximumFiat).toBe(`$${totalMaxFiat}`); + // TODO: test minimum fiat too + // expect(result.current.estimatedMinimumFiat).toBe(`$${totalMaxFiat}`); + act(() => { + result.current.setMaxFeePerGas('90'); + }); + totalEthGasFee = getTotalCostInETH('90', result.current.gasLimit); + totalMaxFiat = ( + Number(totalEthGasFee) * MOCK_ETH_USD_CONVERSION_RATE + ).toFixed(2); + expect(result.current.maxFeePerGas).toBe('90'); + expect(result.current.estimatedMaximumNative).toBe( + `${totalEthGasFee} ETH`, + ); + expect(result.current.estimatedMaximumFiat).toBe(`$${totalMaxFiat}`); + // TODO: test minimum fiat too + // expect(result.current.estimatedMinimumFiat).toBe(`$${totalMaxFiat}`); + }); + }); +}); diff --git a/ui/hooks/useIncrementedGasFees.js b/ui/hooks/useIncrementedGasFees.js new file mode 100644 index 000000000..48b7c6263 --- /dev/null +++ b/ui/hooks/useIncrementedGasFees.js @@ -0,0 +1,73 @@ +import { addHexPrefix } from 'ethereumjs-util'; +import { useMemo } from 'react'; +import { multiplyCurrencies } from '../../shared/modules/conversion.utils'; +import { isEIP1559Transaction } from '../../shared/modules/transaction.utils'; + +/** + * Simple helper to save on duplication to multiply the supplied wei hex string + * by 1.10 to get bare minimum new gas fee. + * + * @param {string} hexStringValue - hex value in wei to be incremented + * @returns {string} - hex value in WEI 10% higher than the param. + */ +function addTenPercent(hexStringValue) { + return addHexPrefix( + multiplyCurrencies(hexStringValue, 1.1, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + }), + ); +} + +/** + * When initializing cancellations or speed ups we need to set the baseline + * gas fees to be 10% higher, which is the bare minimum that the network will + * accept for transactions of the same nonce. Anything lower than this will be + * discarded by the network to avoid DoS attacks. This hook returns an object + * that either has gasPrice or maxFeePerGas/maxPriorityFeePerGas specified. In + * addition the gasLimit will also be included. + * @param {} transactionGroup + * @returns {import( + * '../../app/scripts/controllers/transactions' + * ).CustomGasSettings} - Gas settings for cancellations/speed ups + */ +export function useIncrementedGasFees(transactionGroup) { + const { primaryTransaction } = transactionGroup; + + // We memoize this value so that it can be relied upon in other hooks. + const customGasSettings = useMemo(() => { + // This hook is called indiscriminantly on all transactions appearing in + // the activity list. This includes transitional items such as signature + // requests. These types of "transactions" are not really transactions and + // do not have txParams. This is why we use optional chaining on the + // txParams object in this hook. + const temporaryGasSettings = { + gasLimit: primaryTransaction.txParams?.gas, + }; + if (isEIP1559Transaction(primaryTransaction)) { + const transactionMaxFeePerGas = primaryTransaction.txParams?.maxFeePerGas; + const transactionMaxPriorityFeePerGas = + primaryTransaction.txParams?.maxPriorityFeePerGas; + temporaryGasSettings.maxFeePerGas = + transactionMaxFeePerGas === undefined || + transactionMaxFeePerGas.startsWith('-') + ? '0x0' + : addTenPercent(transactionMaxFeePerGas); + temporaryGasSettings.maxPriorityFeePerGas = + transactionMaxPriorityFeePerGas === undefined || + transactionMaxPriorityFeePerGas.startsWith('-') + ? '0x0' + : addTenPercent(transactionMaxPriorityFeePerGas); + } else { + const transactionGasPrice = primaryTransaction.txParams?.gasPrice; + temporaryGasSettings.gasPrice = + transactionGasPrice === undefined || transactionGasPrice.startsWith('-') + ? '0x0' + : addTenPercent(transactionGasPrice); + } + return temporaryGasSettings; + }, [primaryTransaction]); + + return customGasSettings; +} diff --git a/ui/hooks/useRetryTransaction.js b/ui/hooks/useRetryTransaction.js index 44fccb2f4..9f6b1680c 100644 --- a/ui/hooks/useRetryTransaction.js +++ b/ui/hooks/useRetryTransaction.js @@ -1,27 +1,33 @@ import { useDispatch, useSelector } from 'react-redux'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { showSidebar } from '../store/actions'; -import { - fetchBasicGasEstimates, - setCustomGasPriceForRetry, - setCustomGasLimit, -} from '../ducks/gas/gas.duck'; -import { increaseLastGasPrice } from '../helpers/utils/confirm-tx.util'; +import { setCustomGasLimit, setCustomGasPrice } from '../ducks/gas/gas.duck'; import { getIsMainnet } from '../selectors'; +import { isLegacyTransaction } from '../../shared/modules/transaction.utils'; import { useMetricEvent } from './useMetricEvent'; +import { useIncrementedGasFees } from './useIncrementedGasFees'; + +/** + * @typedef {Object} RetryTransactionReturnValue + * @property {(event: Event) => void} retryTransaction - open edit gas popover + * to begin setting retry gas fees + * @property {boolean} showRetryEditGasPopover - Whether to show the popover + * @property {() => void} closeRetryEditGasPopover - close the popover. + */ + /** * Provides a reusable hook that, given a transactionGroup, will return * a method for beginning the retry process * @param {Object} transactionGroup - the transaction group - * @return {Function} + * @return {RetryTransactionReturnValue} */ export function useRetryTransaction(transactionGroup) { const { primaryTransaction } = transactionGroup; const isMainnet = useSelector(getIsMainnet); + const hideBasic = !(isMainnet || process.env.IN_TEST); - // Signature requests do not have a txParams, but this hook is called indiscriminately - const gasPrice = primaryTransaction.txParams?.gasPrice; + const customGasSettings = useIncrementedGasFees(transactionGroup); const trackMetricsEvent = useMetricEvent({ eventOpts: { category: 'Navigation', @@ -30,31 +36,47 @@ export function useRetryTransaction(transactionGroup) { }, }); const dispatch = useDispatch(); + const [showRetryEditGasPopover, setShowRetryEditGasPopover] = useState(false); + + const closeRetryEditGasPopover = () => setShowRetryEditGasPopover(false); const retryTransaction = useCallback( async (event) => { event.stopPropagation(); trackMetricsEvent(); - await dispatch(fetchBasicGasEstimates); - const transaction = primaryTransaction; - const increasedGasPrice = increaseLastGasPrice(gasPrice); - await dispatch( - setCustomGasPriceForRetry( - increasedGasPrice || transaction.txParams.gasPrice, - ), - ); - dispatch(setCustomGasLimit(transaction.txParams.gas)); - dispatch( - showSidebar({ - transitionName: 'sidebar-left', - type: 'customize-gas', - props: { transaction, hideBasic }, - }), - ); + if (process.env.SHOW_EIP_1559_UI) { + setShowRetryEditGasPopover(true); + } else { + if (isLegacyTransaction(primaryTransaction)) { + // To support the current process of cancelling or speeding up + // a transaction, we have to inform the custom gas state of the new + // gasPrice to start at. + dispatch(setCustomGasPrice(customGasSettings.gasPrice)); + dispatch(setCustomGasLimit(primaryTransaction.txParams.gas)); + } + + dispatch( + showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { transaction: primaryTransaction, hideBasic }, + }), + ); + } }, - [dispatch, trackMetricsEvent, gasPrice, primaryTransaction, hideBasic], + [ + dispatch, + trackMetricsEvent, + customGasSettings, + primaryTransaction, + hideBasic, + ], ); - return retryTransaction; + return { + retryTransaction, + showRetryEditGasPopover, + closeRetryEditGasPopover, + }; } diff --git a/ui/hooks/useRetryTransaction.test.js b/ui/hooks/useRetryTransaction.test.js index 3c1f78401..2f5ec0be9 100644 --- a/ui/hooks/useRetryTransaction.test.js +++ b/ui/hooks/useRetryTransaction.test.js @@ -8,6 +8,10 @@ import * as methodDataHook from './useMethodData'; import * as metricEventHook from './useMetricEvent'; import { useRetryTransaction } from './useRetryTransaction'; +jest.mock('./useGasFeeEstimates', () => ({ + useGasFeeEstimates: jest.fn(), +})); + describe('useRetryTransaction', () => { describe('when transaction meets retry enabled criteria', () => { let useSelector; @@ -53,8 +57,8 @@ describe('useRetryTransaction', () => { const { result } = renderHook(() => useRetryTransaction(retryEnabledTransaction, true), ); - const retry = result.current; - retry(event); + const { retryTransaction } = result.current; + retryTransaction(event); expect(trackEvent.calledOnce).toStrictEqual(true); }); @@ -62,8 +66,8 @@ describe('useRetryTransaction', () => { const { result } = renderHook(() => useRetryTransaction(retryEnabledTransaction, true), ); - const retry = result.current; - await retry(event); + const { retryTransaction } = result.current; + await retryTransaction(event); expect( dispatch.calledWith( showSidebar({ @@ -108,8 +112,8 @@ describe('useRetryTransaction', () => { const { result } = renderHook(() => useRetryTransaction(cancelledTransaction, true), ); - const retry = result.current; - await retry(event); + const { retryTransaction } = result.current; + await retryTransaction(event); expect( dispatch.calledWith( showSidebar({ diff --git a/ui/hooks/useShouldAnimateGasEstimations.js b/ui/hooks/useShouldAnimateGasEstimations.js new file mode 100644 index 000000000..df588c0ec --- /dev/null +++ b/ui/hooks/useShouldAnimateGasEstimations.js @@ -0,0 +1,28 @@ +import { useRef } from 'react'; +import { isEqual } from 'lodash'; + +import { useGasFeeEstimates } from './useGasFeeEstimates'; + +export function useShouldAnimateGasEstimations() { + const { isGasEstimatesLoading, gasFeeEstimates } = useGasFeeEstimates(); + + // Do the animation only when gas prices have changed... + const lastGasEstimates = useRef(gasFeeEstimates); + const gasEstimatesChanged = !isEqual( + lastGasEstimates.current, + gasFeeEstimates, + ); + + // ... and only if gas didn't just load + // Removing this line will cause the initial loading screen to stay empty + const gasJustLoaded = isEqual(lastGasEstimates.current, {}); + + if (gasEstimatesChanged) { + lastGasEstimates.current = gasFeeEstimates; + } + + const showLoadingAnimation = + isGasEstimatesLoading || (gasEstimatesChanged && !gasJustLoaded); + + return showLoadingAnimation; +} diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index 1fad5d2c7..0677e611e 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -1,5 +1,4 @@ -import { useSelector } from 'react-redux'; -import { captureException } from '@sentry/browser'; +import { useDispatch, useSelector } from 'react-redux'; import { getKnownMethodData } from '../selectors/selectors'; import { getStatusKey, @@ -23,6 +22,7 @@ import { TRANSACTION_GROUP_CATEGORIES, TRANSACTION_STATUSES, } from '../../shared/constants/transaction'; +import { captureSingleException } from '../store/actions'; import { useI18nContext } from './useI18nContext'; import { useTokenFiatAmount } from './useTokenFiatAmount'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; @@ -58,6 +58,7 @@ import { useCurrentAsset } from './useCurrentAsset'; export function useTransactionDisplayData(transactionGroup) { // To determine which primary currency to display for swaps transactions we need to be aware // of which asset, if any, we are viewing at present + const dispatch = useDispatch(); const currentAsset = useCurrentAsset(); const knownTokens = useSelector(getTokens); const t = useI18nContext(); @@ -222,8 +223,8 @@ export function useTransactionDisplayData(transactionGroup) { title = t('send'); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); } else { - captureException( - Error( + dispatch( + captureSingleException( `useTransactionDisplayData does not recognize transaction type. Type received is: ${type}`, ), ); diff --git a/ui/hooks/useTransactionDisplayData.test.js b/ui/hooks/useTransactionDisplayData.test.js index 3b36c255b..fad81f0e0 100644 --- a/ui/hooks/useTransactionDisplayData.test.js +++ b/ui/hooks/useTransactionDisplayData.test.js @@ -132,6 +132,8 @@ const renderHookWithRouter = (cb, tokenAddress) => { }; describe('useTransactionDisplayData', () => { + const dispatch = sinon.spy(); + beforeAll(() => { useSelector = sinon.stub(reactRedux, 'useSelector'); useTokenFiatAmount = sinon.stub( @@ -169,6 +171,7 @@ describe('useTransactionDisplayData', () => { } return null; }); + sinon.stub(reactRedux, 'useDispatch').returns(dispatch); }); afterAll(() => { diff --git a/ui/pages/add-token/add-token.component.js b/ui/pages/add-token/add-token.component.js index 36c215e2a..57852de20 100644 --- a/ui/pages/add-token/add-token.component.js +++ b/ui/pages/add-token/add-token.component.js @@ -9,7 +9,7 @@ import PageContainer from '../../components/ui/page-container'; import { Tabs, Tab } from '../../components/ui/tabs'; import { addHexPrefix } from '../../../app/scripts/lib/util'; import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; -import ActionableMessage from '../swaps/actionable-message'; +import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; import Typography from '../../components/ui/typography'; import { TYPOGRAPHY, FONT_WEIGHT } from '../../helpers/constants/design-system'; import Button from '../../components/ui/button'; diff --git a/ui/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/pages/add-token/token-list/token-list-placeholder/index.scss index aa6d0a0e9..bbc1e8304 100644 --- a/ui/pages/add-token/token-list/token-list-placeholder/index.scss +++ b/ui/pages/add-token/token-list/token-list-placeholder/index.scss @@ -4,13 +4,17 @@ padding-top: 36px; flex-direction: column; line-height: 22px; - opacity: 0.5; + + img { + opacity: 0.5; + } &__text { color: $silver-chalice; width: 50%; text-align: center; margin-top: 8px; + opacity: 0.5; @media screen and (max-width: 575px) { width: 60%; diff --git a/ui/pages/asset/asset.js b/ui/pages/asset/asset.js index cafdb2285..f10743a76 100644 --- a/ui/pages/asset/asset.js +++ b/ui/pages/asset/asset.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { Redirect, useParams } from 'react-router-dom'; import { getTokens } from '../../ducks/metamask/metamask'; @@ -14,6 +14,11 @@ const Asset = () => { const token = tokens.find(({ address }) => address === asset); + useEffect(() => { + const el = document.querySelector('.app'); + el.scroll(0, 0); + }, []); + let content; if (token) { content = ; diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 05bb4c77b..e8230a9f5 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -303,7 +303,7 @@ export default class ConfirmApproveContent extends Component { >
    - View full transaction details + {t('viewFullTransactionDetails')}
    - View full transaction details + {t('viewFullTransactionDetails')}
    { + const options = []; + const receiverOptions = { + 'Address 1': '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + 'Address 2': '0x55e0bfb2d400e9be8cf9b114e38a40969a02f69a', + }; + const state = store.getState(); + const { identities } = state.metamask; + Object.keys(identities).forEach(function (key) { + options.push({ + label: identities[key].name, + address: key, + }); + }); + const sender = select('Sender', options, options[0]); + const receiver = select( + 'Receiver', + receiverOptions, + '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + ); + + const confirmTransactionState = state.confirmTransaction.txData.txParams; + + useEffect(() => { + confirmTransactionState.from = sender.address; + store.dispatch(updateTransactionParams(id, confirmTransactionState)); + }, [sender, confirmTransactionState]); + + useEffect(() => { + confirmTransactionState.to = receiver; + store.dispatch(updateTransactionParams(id, confirmTransactionState)); + }, [receiver, confirmTransactionState]); + return children; +}; + +export const DeployContract = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js index 358c71f3c..3be8d21ff 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -7,7 +7,7 @@ import Identicon from '../../components/ui/identicon'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; -import { conversionUtil } from '../../helpers/utils/conversion-util'; +import { conversionUtil } from '../../../shared/modules/conversion.utils'; export default class ConfirmEncryptionPublicKey extends Component { static contextTypes = { @@ -33,10 +33,6 @@ export default class ConfirmEncryptionPublicKey extends Component { nativeCurrency: PropTypes.string.isRequired, }; - state = { - fromAccount: this.props.fromAccount, - }; - componentDidMount = () => { if ( getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION @@ -92,7 +88,7 @@ export default class ConfirmEncryptionPublicKey extends Component { }; renderAccount = () => { - const { fromAccount } = this.state; + const { fromAccount } = this.props; const { t } = this.context; return ( @@ -109,11 +105,12 @@ export default class ConfirmEncryptionPublicKey extends Component { }; renderBalance = () => { - const { conversionRate, nativeCurrency } = this.props; - const { t } = this.context; const { + conversionRate, + nativeCurrency, fromAccount: { balance }, - } = this.state; + } = this.props; + const { t } = this.context; const nativeCurrencyBalance = conversionUtil(balance, { fromNumericBase: 'hex', diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js new file mode 100644 index 000000000..548097850 --- /dev/null +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js @@ -0,0 +1,45 @@ +import React, { useEffect } from 'react'; +import { select } from '@storybook/addon-knobs'; + +import { store } from '../../../.storybook/preview'; +import { updateMetamaskState } from '../../store/actions'; +import ConfirmEncryptionPublicKey from '.'; + +export default { + title: 'Confirmation Screens', +}; + +const PageSet = ({ children }) => { + const state = store.getState(); + const options = []; + const { identities, unapprovedEncryptionPublicKeyMsgs } = state.metamask; + Object.keys(identities).forEach(function (key) { + options.push({ + label: identities[key].name, + name: identities[key].name, + address: key, + }); + }); + const account = select('Account', options, options[0]); + + useEffect(() => { + unapprovedEncryptionPublicKeyMsgs['7786962153682822'].msgParams = + account.address; + store.dispatch( + updateMetamaskState({ + unapprovedEncryptionPublicKeyMsgs, + }), + ); + }, [account, unapprovedEncryptionPublicKeyMsgs]); + + return children; +}; + +export const ConfirmEncryption = () => { + store.dispatch(updateMetamaskState({ unapprovedTxs: {} })); + return ( + + + + ); +}; diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/pages/confirm-send-ether/confirm-send-ether.component.js index cbdbebb91..a1166fb03 100644 --- a/ui/pages/confirm-send-ether/confirm-send-ether.component.js +++ b/ui/pages/confirm-send-ether/confirm-send-ether.component.js @@ -16,8 +16,9 @@ export default class ConfirmSendEther extends Component { handleEdit({ txData }) { const { editTransaction, history } = this.props; - editTransaction(txData); - history.push(SEND_ROUTE); + editTransaction(txData).then(() => { + history.push(SEND_ROUTE); + }); } shouldHideData() { diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/pages/confirm-send-ether/confirm-send-ether.container.js index 5f7527226..a637fbb12 100644 --- a/ui/pages/confirm-send-ether/confirm-send-ether.container.js +++ b/ui/pages/confirm-send-ether/confirm-send-ether.container.js @@ -17,9 +17,9 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - editTransaction: (txData) => { + editTransaction: async (txData) => { const { id } = txData; - dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString())); + await dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString())); dispatch(clearConfirmTransaction()); }, }; diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js index b15a5f99e..557b6c237 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -23,6 +23,7 @@ export default function ConfirmTokenTransactionBase({ contractExchangeRate, conversionRate, currentCurrency, + onEdit, }) { const t = useContext(I18nContext); @@ -69,6 +70,7 @@ export default function ConfirmTokenTransactionBase({ return ( + ) : ( + + updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice }) + } + updateCustomGasLimit={(newGasLimit) => + updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit }) + } + customGasPrice={customGas.gasPrice} + customGasLimit={customGas.gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe + isSpeedUp={false} + /> + ); + + const nonceField = useNonceField ? ( +
    +
    +
    + {t('nonceFieldHeading')} +
    +
    + { + if (!value.length || Number(value) < 0) { + updateCustomNonce(''); + } else { + updateCustomNonce(String(Math.floor(value))); + } + getNextNonce(); + }} + fullWidth + margin="dense" + value={customNonceValue || ''} + /> +
    +
    +
    + ) : null; + + const showInlineControls = process.env.SHOW_EIP_1559_UI + ? advancedInlineGasShown + : advancedInlineGasShown || notMainnetOrTest || gasPriceFetchFailure; + + const showGasEditButton = process.env.SHOW_EIP_1559_UI + ? !showInlineControls + : !(notMainnetOrTest || gasPriceFetchFailure); + + if (process.env.SHOW_EIP_1559_UI) { + return ( +
    + this.handleEditGas()} + rows={[ + + {t('transactionDetailDappGasHeading', [txData.origin])} + + + + + ) : ( + <> + {t('transactionDetailGasHeading')} + +

    {t('transactionDetailGasTooltipIntro')}

    +

    {t('transactionDetailGasTooltipExplanation')}

    +

    + + {t('transactionDetailGasTooltipConversion')} + +

    + + } + position="top" + > + +
    + + ) + } + detailTitleColor={ + txData.dappSuggestedGasFees ? COLORS.SECONDARY1 : COLORS.BLACK + } + detailText={ + + } + detailTotal={ + + } + subText={t('editGasSubTextFee', [ + , + ])} + subTitle={ + + } + />, + + } + detailTotal={ + + } + subTitle={t('transactionDetailGasTotalSubtitle')} + subText={t('editGasSubTextAmount', [ + , + ])} + />, + ]} + /> + {nonceField} +
    + ); + } + return (
    this.handleEditGas() + showGasEditButton ? () => this.handleEditGas() : null } secondaryText={ - hideFiatConversion - ? this.context.t('noConversionRateAvailable') - : '' + hideFiatConversion ? t('noConversionRateAvailable') : '' } /> - {advancedInlineGasShown || - notMainnetOrTest || - gasPriceFetchFailure ? ( - - updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice }) - } - updateCustomGasLimit={(newGasLimit) => - updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit }) - } - customGasPrice={customGas.gasPrice} - customGasLimit={customGas.gasLimit} - insufficientBalance={insufficientBalance} - customPriceIsSafe - isSpeedUp={false} - /> - ) : null} + {showInlineControls ? inlineGasControls : null} {noGasPrice ? (
    @@ -334,48 +506,20 @@ export default class ConfirmTransactionBase extends Component { } >
    - {useNonceField ? ( -
    -
    -
    - {this.context.t('nonceFieldHeading')} -
    -
    - { - if (!value.length || Number(value) < 0) { - updateCustomNonce(''); - } else { - updateCustomNonce(String(Math.floor(value))); - } - getNextNonce(); - }} - fullWidth - margin="dense" - value={customNonceValue || ''} - /> -
    -
    -
    - ) : null} + {nonceField}
    ); } @@ -645,7 +789,6 @@ export default class ConfirmTransactionBase extends Component { render() { const { t } = this.context; const { - isTxReprice, fromName, fromAddress, toName, @@ -672,6 +815,7 @@ export default class ConfirmTransactionBase extends Component { submitError, submitWarning, ethGasPriceWarning, + editingGas, } = this.state; const { name } = methodData; @@ -705,7 +849,7 @@ export default class ConfirmTransactionBase extends Component { toAddress={toAddress} toEns={toEns} toNickname={toNickname} - showEdit={onEdit && !isTxReprice} + showEdit={Boolean(onEdit)} action={functionType} title={title} titleComponent={this.renderTitleComponent()} @@ -739,6 +883,9 @@ export default class ConfirmTransactionBase extends Component { hideSenderToRecipient={hideSenderToRecipient} origin={txData.origin} ethGasPriceWarning={ethGasPriceWarning} + editingGas={editingGas} + handleCloseEditGas={() => this.handleCloseNewGasPopover()} + currentTransaction={txData} /> ); } diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 09361ad93..df2a0b670 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -10,7 +10,6 @@ import { cancelTxs, updateAndApproveTx, showModal, - updateTransaction, getNextNonce, tryReverseResolveAddress, setDefaultHomeActiveTabName, @@ -21,7 +20,7 @@ import { } from '../../helpers/constants/error-keys'; import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util'; import { isBalanceSufficient, calcGasTotal } from '../send/send.utils'; -import { conversionGreaterThan } from '../../helpers/utils/conversion-util'; +import { conversionGreaterThan } from '../../../shared/modules/conversion.utils'; import { MIN_GAS_LIMIT_DEC } from '../send/send.constants'; import { shortenAddress, valuesFor } from '../../helpers/utils/util'; import { @@ -39,6 +38,7 @@ import { import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; +import { updateTransactionGasFees } from '../../ducks/metamask/metamask'; import ConfirmTransactionBase from './confirm-transaction-base.component'; const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { @@ -78,7 +78,7 @@ const mapStateToProps = (state, ownProps) => { provider: { chainId }, } = metamask; const { tokenData, txData, tokenProps, nonce } = confirmTransaction; - const { txParams = {}, lastGasPrice, id: transactionId, type } = txData; + const { txParams = {}, id: transactionId, type } = txData; const transaction = Object.values(unapprovedTxs).find( ({ id }) => id === (transactionId || Number(paramsTransactionId)), @@ -107,7 +107,6 @@ const mapStateToProps = (state, ownProps) => { const addressBookObject = addressBook[checksummedAddress]; const toEns = ensResolutionsByAddress[checksummedAddress] || ''; const toNickname = addressBookObject ? addressBookObject.name : ''; - const isTxReprice = Boolean(lastGasPrice); const transactionStatus = transaction ? transaction.status : ''; const { @@ -165,7 +164,6 @@ const mapStateToProps = (state, ownProps) => { tokenData, methodData, tokenProps, - isTxReprice, conversionRate, transactionStatus, nonce, @@ -215,9 +213,6 @@ export const mapDispatchToProps = (dispatch) => { }), ); }, - updateGasAndCalculate: (updatedTx) => { - return dispatch(updateTransaction(updatedTx)); - }, showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount, @@ -233,6 +228,9 @@ export const mapDispatchToProps = (dispatch) => { getNextNonce: () => dispatch(getNextNonce()), setDefaultHomeActiveTabName: (tabName) => dispatch(setDefaultHomeActiveTabName(tabName)), + updateTransactionGasFees: (gasFees) => { + dispatch(updateTransactionGasFees({ ...gasFees, expectHexWei: true })); + }, }; }; @@ -288,7 +286,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const { cancelAllTransactions: dispatchCancelAllTransactions, showCustomizeGasModal: dispatchShowCustomizeGasModal, - updateGasAndCalculate: dispatchUpdateGasAndCalculate, + updateTransactionGasFees: dispatchUpdateTransactionGasFees, ...otherDispatchProps } = dispatchProps; @@ -305,21 +303,17 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { showCustomizeGasModal: () => dispatchShowCustomizeGasModal({ txData, - onSubmit: (customGas) => dispatchUpdateGasAndCalculate(customGas), + onSubmit: (customGas) => dispatchUpdateTransactionGasFees(customGas), validate: validateEditGas, }), cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)), updateGasAndCalculate: ({ gasLimit, gasPrice }) => { - const updatedTx = { - ...txData, - txParams: { - ...txData.txParams, - gas: gasLimit, - gasPrice, - }, - }; - dispatchUpdateGasAndCalculate(updatedTx); + dispatchUpdateTransactionGasFees({ + gasLimit, + gasPrice, + transaction: txData, + }); }, }; }; diff --git a/ui/pages/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirm-transaction/confirm-transaction.component.js index 5bc374edc..2988739e6 100644 --- a/ui/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirm-transaction/confirm-transaction.component.js @@ -25,6 +25,10 @@ import { ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, DEFAULT_ROUTE, } from '../../helpers/constants/routes'; +import { + disconnectGasFeeEstimatePoller, + getGasFeeEstimatesAndStartPolling, +} from '../../store/actions'; import ConfTx from './conf-tx'; export default class ConfirmTransaction extends Component { @@ -38,7 +42,6 @@ export default class ConfirmTransaction extends Component { sendTo: PropTypes.string, setTransactionToConfirm: PropTypes.func, clearConfirmTransaction: PropTypes.func, - fetchBasicGasEstimates: PropTypes.func, mostRecentOverviewPage: PropTypes.string.isRequired, transaction: PropTypes.object, getContractMethodData: PropTypes.func, @@ -49,14 +52,19 @@ export default class ConfirmTransaction extends Component { setDefaultHomeActiveTabName: PropTypes.func, }; + constructor(props) { + super(props); + this.state = {}; + } + componentDidMount() { + this._isMounted = true; const { totalUnapprovedCount = 0, sendTo, history, mostRecentOverviewPage, transaction: { txParams: { data, to } = {} } = {}, - fetchBasicGasEstimates, getContractMethodData, transactionId, paramsTransactionId, @@ -64,12 +72,19 @@ export default class ConfirmTransaction extends Component { isTokenMethodAction, } = this.props; + getGasFeeEstimatesAndStartPolling().then((pollingToken) => { + if (this._isMounted) { + this.setState({ pollingToken }); + } else { + disconnectGasFeeEstimatePoller(pollingToken); + } + }); + if (!totalUnapprovedCount && !sendTo) { history.replace(mostRecentOverviewPage); return; } - fetchBasicGasEstimates(); getContractMethodData(data); if (isTokenMethodAction) { getTokenParams(to); @@ -80,6 +95,13 @@ export default class ConfirmTransaction extends Component { } } + componentWillUnmount() { + this._isMounted = false; + if (this.state.pollingToken) { + disconnectGasFeeEstimatePoller(this.state.pollingToken); + } + } + componentDidUpdate(prevProps) { const { setTransactionToConfirm, diff --git a/ui/pages/confirm-transaction/confirm-transaction.container.js b/ui/pages/confirm-transaction/confirm-transaction.container.js index bf0020c64..da7b04f18 100644 --- a/ui/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/pages/confirm-transaction/confirm-transaction.container.js @@ -6,7 +6,6 @@ import { clearConfirmTransaction, } from '../../ducks/confirm-transaction/confirm-transaction.duck'; import { isTokenMethodAction } from '../../helpers/utils/transactions.util'; -import { fetchBasicGasEstimates } from '../../ducks/gas/gas.duck'; import { getContractMethodData, @@ -54,7 +53,6 @@ const mapDispatchToProps = (dispatch) => { dispatch(setTransactionToConfirm(transactionId)); }, clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), - fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), getContractMethodData: (data) => dispatch(getContractMethodData(data)), getTokenParams: (tokenAddress) => dispatch(getTokenParams(tokenAddress)), setDefaultHomeActiveTabName: (tabName) => diff --git a/ui/pages/create-account/import-account/index.scss b/ui/pages/create-account/import-account/index.scss index 11056d25d..1e5efe7f0 100644 --- a/ui/pages/create-account/import-account/index.scss +++ b/ui/pages/create-account/import-account/index.scss @@ -14,6 +14,10 @@ align-items: center; padding: 0 30px 30px; + @media screen and (max-width: $break-small) { + padding: 0 22px 22px; + } + &__select-section { display: flex; justify-content: space-between; diff --git a/ui/pages/keychains/restore-vault.js b/ui/pages/keychains/restore-vault.js index 4ae63dbbe..625b9c71c 100644 --- a/ui/pages/keychains/restore-vault.js +++ b/ui/pages/keychains/restore-vault.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { ethers } from 'ethers'; import { createNewVaultAndRestore, unMarkPasswordForgotten, @@ -10,6 +11,8 @@ import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; import TextField from '../../components/ui/text-field'; import Button from '../../components/ui/button'; +const { isValidMnemonic } = ethers.utils; + class RestoreVaultPage extends Component { static contextTypes = { t: PropTypes.func, @@ -38,6 +41,7 @@ class RestoreVaultPage extends Component { (seedPhrase || '').trim().toLowerCase().match(/\w+/gu)?.join(' ') || ''; handleSeedPhraseChange(seedPhrase) { + const { t } = this.context; let seedPhraseError = null; const wordCount = this.parseSeedPhrase(seedPhrase).split(/\s/u).length; @@ -45,7 +49,9 @@ class RestoreVaultPage extends Component { seedPhrase && (wordCount % 3 !== 0 || wordCount < 12 || wordCount > 24) ) { - seedPhraseError = this.context.t('seedPhraseReq'); + seedPhraseError = t('seedPhraseReq'); + } else if (!isValidMnemonic(seedPhrase)) { + seedPhraseError = t('invalidSeedPhrase'); } this.setState({ seedPhrase, seedPhraseError }); diff --git a/ui/pages/mobile-sync/mobile-sync.component.js b/ui/pages/mobile-sync/mobile-sync.component.js index 683310ba0..e03e6eab2 100644 --- a/ui/pages/mobile-sync/mobile-sync.component.js +++ b/ui/pages/mobile-sync/mobile-sync.component.js @@ -296,7 +296,7 @@ export default class MobileSyncPage extends Component { const { t } = this.context; if (syncing) { - return ; + return ; } if (completed) { diff --git a/ui/pages/onboarding-flow/new-account/index.scss b/ui/pages/onboarding-flow/new-account/index.scss new file mode 100644 index 000000000..4c8e69bf2 --- /dev/null +++ b/ui/pages/onboarding-flow/new-account/index.scss @@ -0,0 +1,34 @@ +.new-account { + &__wrapper { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + } + + &__link-text { + color: $primary-1; + } + + &__form { + padding: 0 24px; + + &--password-button { + background-color: transparent; + } + + &--submit-button { + padding: 20px; + } + + &--checkmark { + i { + color: $success-1; + } + } + + .form-field__input { + height: 50px; + } + } +} diff --git a/ui/pages/onboarding-flow/new-account/new-account.js b/ui/pages/onboarding-flow/new-account/new-account.js new file mode 100644 index 000000000..2eb351ff8 --- /dev/null +++ b/ui/pages/onboarding-flow/new-account/new-account.js @@ -0,0 +1,184 @@ +import React, { useState, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import Button from '../../../components/ui/button'; +import Typography from '../../../components/ui/typography'; +import { + TEXT_ALIGN, + TYPOGRAPHY, + JUSTIFY_CONTENT, + FONT_WEIGHT, + ALIGN_ITEMS, +} from '../../../helpers/constants/design-system'; +import { INITIALIZE_SEED_PHRASE_INTRO_ROUTE } from '../../../helpers/constants/routes'; +import FormField from '../../../components/ui/form-field'; +import Box from '../../../components/ui/box'; +import CheckBox from '../../../components/ui/check-box'; + +export default function NewAccount({ onSubmit }) { + const t = useI18nContext(); + const [confirmPassword, setConfirmPassword] = useState(''); + const [password, setPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [confirmPasswordError, setConfirmPasswordError] = useState(''); + const [termsChecked, setTermsChecked] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const history = useHistory(); + + const submitPasswordEvent = useNewMetricEvent({ + event: 'Submit Password', + category: 'Onboarding', + }); + + const isValid = useMemo(() => { + if (!password || !confirmPassword || password !== confirmPassword) { + return false; + } + + if (password.length < 8) { + return false; + } + + return !passwordError && !confirmPasswordError; + }, [password, confirmPassword, passwordError, confirmPasswordError]); + + const handlePasswordChange = (passwordInput) => { + let error = ''; + let confirmError = ''; + if (passwordInput && passwordInput.length < 8) { + error = t('passwordNotLongEnough'); + } + + if (confirmPassword && passwordInput !== confirmPassword) { + confirmError = t('passwordsDontMatch'); + } + + setPassword(passwordInput); + setPasswordError(error); + setConfirmPasswordError(confirmError); + }; + + const handleConfirmPasswordChange = (confirmPasswordInput) => { + let error = ''; + if (password !== confirmPasswordInput) { + error = t('passwordsDontMatch'); + } + + setConfirmPassword(confirmPasswordInput); + setConfirmPasswordError(error); + }; + + const handleCreate = async (event) => { + event.preventDefault(); + + if (!isValid) { + return; + } + try { + if (onSubmit) { + await onSubmit(password); + } + submitPasswordEvent(); + history.push(INITIALIZE_SEED_PHRASE_INTRO_ROUTE); + } catch (error) { + setPasswordError(error.message); + } + }; + + return ( +
    + + {t('createPassword')} + + + {t('passwordSetupDetails')} + + +
    + { + e.preventDefault(); + setShowPassword(!showPassword); + }} + > + {showPassword ? t('hide') : t('show')} + + } + /> + + +
    + ) + } + /> + + setTermsChecked(!termsChecked)} + checked={termsChecked} + /> + + {t('passwordTermsWarning', [ + e.stopPropagation()} + key="new-account__link-text" + href="https://metamask.io/terms.html" + target="_blank" + rel="noopener noreferrer" + > + + {t('learnMore')} + + , + ])} + + + + + +
    + ); +} + +NewAccount.propTypes = { + onSubmit: PropTypes.func, +}; diff --git a/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js new file mode 100644 index 000000000..52d3926e1 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js @@ -0,0 +1,100 @@ +import React, { useState, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { debounce } from 'lodash'; +import PropTypes from 'prop-types'; +import Box from '../../../components/ui/box'; +import Button from '../../../components/ui/button'; +import Typography from '../../../components/ui/typography'; +import { + TEXT_ALIGN, + TYPOGRAPHY, + JUSTIFY_CONTENT, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; +import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../helpers/constants/routes'; +import ProgressBar from '../../../components/app/step-progress-bar'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import RecoveryPhraseChips from './recovery-phrase-chips'; + +export default function ConfirmRecoveryPhrase({ seedPhrase = '' }) { + const history = useHistory(); + const t = useI18nContext(); + const splitSeedPhrase = seedPhrase.split(' '); + const indicesToCheck = [2, 3, 7]; + const [matching, setMatching] = useState(false); + + // Removes seed phrase words from chips corresponding to the + // indicesToCheck so that user has to complete the phrase and confirm + // they have saved it. + const initializePhraseElements = () => { + const phraseElements = { ...splitSeedPhrase }; + indicesToCheck.forEach((i) => { + phraseElements[i] = ''; + }); + return phraseElements; + }; + const [phraseElements, setPhraseElements] = useState( + initializePhraseElements(), + ); + + const validate = useMemo( + () => + debounce((elements) => { + setMatching(Object.values(elements).join(' ') === seedPhrase); + }, 500), + [setMatching, seedPhrase], + ); + + const handleSetPhraseElements = (values) => { + setPhraseElements(values); + validate(values); + }; + + return ( +
    + + + + {t('seedPhraseConfirm')} + + + + + {t('seedPhraseEnterMissingWords')} + + + +
    + +
    +
    + ); +} + +ConfirmRecoveryPhrase.propTypes = { + seedPhrase: PropTypes.string, +}; diff --git a/ui/pages/onboarding-flow/recovery-phrase/index.scss b/ui/pages/onboarding-flow/recovery-phrase/index.scss new file mode 100644 index 000000000..7c37d0059 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/index.scss @@ -0,0 +1,110 @@ +.recovery-phrase { + &__tips { + flex-direction: column; + + ul { + list-style: disc; + margin-left: 20px; + } + } + + &__chips { + display: grid; + grid-template-columns: 160px 160px 160px; + justify-items: center; + align-items: center; + row-gap: 16px; + + &--hidden { + filter: blur(5px); + } + } + + &__secret { + position: relative; + } + + &__secret-blocker { + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + padding: 8px 0 18px; + border-radius: 4px; + color: $ui-white; + + &--text { + margin-top: 32px; + } + } + + &__chip-item { + display: flex; + flex-direction: row; + align-items: center; + text-align: center; + + &__number { + font-size: $font-size-h5; + } + } + + &__footer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + &--button { + width: 50%; + padding: 20px; + } + + &--copy { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + &--button { + background-color: transparent; + border: none; + display: flex; + justify-content: space-evenly; + width: 40%; + color: $primary-blue; + cursor: pointer; + margin-bottom: 24px; + + &:active { + color: $ui-black; + background-color: transparent; + border: none; + transform: scale(0.97); + } + } + } + } + + &__chip { + justify-content: center; + border-radius: 13px; + height: 32px; + width: 120px; + + &--with-input { + width: 120px; + box-shadow: 0 3px 4px -3px $Grey-800; + border-width: 2px; + border-radius: 13px; + height: 32px; + } + } +} diff --git a/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js new file mode 100644 index 000000000..c46193833 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js @@ -0,0 +1,101 @@ +import React from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import Chip from '../../../components/ui/chip'; +import Box from '../../../components/ui/box'; +import Typography from '../../../components/ui/typography'; +import { ChipWithInput } from '../../../components/ui/chip/chip-with-input'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + TYPOGRAPHY, + COLORS, + BORDER_STYLE, + SIZES, + DISPLAY, +} from '../../../helpers/constants/design-system'; + +export default function RecoveryPhraseChips({ + seedPhrase, + seedPhraseRevealed, + confirmPhase, + setInputValue, + inputValue, + indicesToCheck, +}) { + const t = useI18nContext(); + const hideSeedPhrase = seedPhraseRevealed === false; + return ( + +
    + {seedPhrase.map((word, index) => { + if ( + confirmPhase && + indicesToCheck && + indicesToCheck.includes(index) + ) { + return ( +
    +
    + {`${index + 1}.`} +
    + { + setInputValue({ ...inputValue, [index]: value }); + }} + /> +
    + ); + } + return ( +
    +
    + {`${index + 1}.`} +
    + + {word} + +
    + ); + })} +
    + + {hideSeedPhrase && ( +
    + + + {t('makeSureNoOneWatching')} + +
    + )} +
    + ); +} + +RecoveryPhraseChips.propTypes = { + seedPhrase: PropTypes.array, + seedPhraseRevealed: PropTypes.bool, + confirmPhase: PropTypes.bool, + setInputValue: PropTypes.func, + inputValue: PropTypes.string, + indicesToCheck: PropTypes.array, +}; diff --git a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js new file mode 100644 index 000000000..78f8437dc --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import Box from '../../../components/ui/box'; +import Button from '../../../components/ui/button'; +import Typography from '../../../components/ui/typography'; +import Copy from '../../../components/ui/icon/copy-icon.component'; +import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../helpers/constants/routes'; +import { + TEXT_ALIGN, + TYPOGRAPHY, + JUSTIFY_CONTENT, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; +import ProgressBar from '../../../components/app/step-progress-bar'; +import RecoveryPhraseChips from './recovery-phrase-chips'; + +export default function RecoveryPhrase({ seedPhrase }) { + const history = useHistory(); + const t = useI18nContext(); + const [copied, handleCopy] = useCopyToClipboard(); + const [seedPhraseRevealed, setSeedPhraseRevealed] = useState(false); + return ( +
    + + + + {t('seedPhraseWriteDownHeader')} + + + + + {t('seedPhraseWriteDownDetails')} + + + + + {t('tips')}: + +
      +
    • + + {t('seedPhraseIntroSidebarBulletFour')} + +
    • +
    • + + {t('seedPhraseIntroSidebarBulletTwo')} + +
    • +
    • + + {t('seedPhraseIntroSidebarBulletThree')} + +
    • +
    • + + {t('seedPhraseIntroSidebarBulletFour')} + +
    • +
    +
    + +
    + {seedPhraseRevealed ? ( +
    + + +
    + ) : ( + + )} +
    +
    + ); +} + +RecoveryPhrase.propTypes = { + seedPhrase: PropTypes.string, +}; diff --git a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js deleted file mode 100644 index db520fcdc..000000000 --- a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -export default class AmountMaxButton extends Component { - static propTypes = { - balance: PropTypes.string, - buttonDataLoading: PropTypes.bool, - clearMaxAmount: PropTypes.func, - inError: PropTypes.bool, - gasTotal: PropTypes.string, - maxModeOn: PropTypes.bool, - sendToken: PropTypes.object, - setAmountToMax: PropTypes.func, - setMaxModeTo: PropTypes.func, - tokenBalance: PropTypes.string, - }; - - static contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, - }; - - setMaxAmount() { - const { - balance, - gasTotal, - sendToken, - setAmountToMax, - tokenBalance, - } = this.props; - - setAmountToMax({ - balance, - gasTotal, - sendToken, - tokenBalance, - }); - } - - onMaxClick = () => { - const { setMaxModeTo, clearMaxAmount, maxModeOn } = this.props; - const { metricsEvent } = this.context; - - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Edit Screen', - name: 'Clicked "Amount Max"', - }, - }); - if (maxModeOn) { - setMaxModeTo(false); - clearMaxAmount(); - } else { - setMaxModeTo(true); - this.setMaxAmount(); - } - }; - - render() { - const { maxModeOn, buttonDataLoading, inError } = this.props; - - return ( -
    - -
    - {this.context.t('max')} -
    -
    - ); - } -} diff --git a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.js b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.js index 7f143879b..03676e3c5 100644 --- a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.js +++ b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.js @@ -29,17 +29,20 @@ export default function AmountMaxButton() { dispatch(toggleSendMaxMode()); }; + const disabled = process.env.SHOW_EIP_1559_UI + ? isDraftTransactionInvalid + : buttonDataLoading || isDraftTransactionInvalid; + return (