Merge pull request #11274 from MetaMask/Version-v9.7.0

Version v9.7.0 RC
feature/default_network_editable
ryanml 3 years ago committed by GitHub
commit cbb0e4d45c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .circleci/scripts/chrome-install.sh
  2. 2
      .github/ISSUE_TEMPLATE/config.yml
  3. 3
      .github/workflows/cla.yml
  4. 56
      .storybook/initial-states/approval-screens/token-approval.js
  5. 24
      .storybook/metametrics.js
  6. 40
      .storybook/preview.js
  7. 939
      .storybook/test-data.js
  8. 19
      CHANGELOG.md
  9. 75
      app/_locales/en/messages.json
  10. 15
      app/_locales/es/messages.json
  11. 15
      app/_locales/es_419/messages.json
  12. 18
      app/_locales/hi/messages.json
  13. 18
      app/_locales/id/messages.json
  14. 12
      app/_locales/it/messages.json
  15. 15
      app/_locales/ja/messages.json
  16. 18
      app/_locales/ko/messages.json
  17. 15
      app/_locales/ph/messages.json
  18. 15
      app/_locales/pt_BR/messages.json
  19. 18
      app/_locales/ru/messages.json
  20. 3
      app/_locales/tl/messages.json
  21. 18
      app/_locales/vi/messages.json
  22. 9
      app/_locales/zh_CN/messages.json
  23. 3
      app/scripts/background.js
  24. 26
      app/scripts/controllers/app-state.js
  25. 3
      app/scripts/controllers/detect-tokens.js
  26. 3
      app/scripts/controllers/incoming-transactions.js
  27. 3
      app/scripts/controllers/incoming-transactions.test.js
  28. 5
      app/scripts/controllers/network/createJsonRpcClient.js
  29. 13
      app/scripts/controllers/network/network.js
  30. 3
      app/scripts/controllers/network/pending-middleware.test.js
  31. 8
      app/scripts/controllers/swaps.js
  32. 9
      app/scripts/controllers/swaps.test.js
  33. 5
      app/scripts/controllers/token-rates.js
  34. 6
      app/scripts/controllers/transactions/index.js
  35. 3
      app/scripts/controllers/transactions/index.test.js
  36. 52
      app/scripts/lib/ComposableObservableStore.js
  37. 174
      app/scripts/lib/ComposableObservableStore.test.js
  38. 3
      app/scripts/lib/ens-ipfs/setup.js
  39. 3
      app/scripts/lib/network-store.js
  40. 3
      app/scripts/lib/segment.js
  41. 142
      app/scripts/metamask-controller.js
  42. 38
      app/scripts/metamask-controller.test.js
  43. 32
      app/scripts/migrations/061.js
  44. 67
      app/scripts/migrations/061.test.js
  45. 1
      app/scripts/migrations/index.js
  46. 4
      app/scripts/platforms/extension.js
  47. 134
      development/lib/run-command.js
  48. 50
      development/sentry-publish.js
  49. 1
      development/sentry-upload-artifacts.sh
  50. 2
      jest.config.js
  51. 17
      jsconfig.json
  52. 16
      package.json
  53. 11
      shared/constants/gas.js
  54. 2
      shared/constants/swaps.js
  55. 5
      shared/constants/time.js
  56. 11
      shared/modules/fetch-with-timeout.test.js
  57. 3
      shared/modules/hexstring-utils.js
  58. 3
      shared/modules/rpc.utils.js
  59. 96
      shared/modules/tests/transaction.utils.test.js
  60. 31
      shared/modules/transaction.utils.js
  61. 151
      test/e2e/fixtures/custom-token/state.json
  62. 14
      test/e2e/helpers.js
  63. 51
      test/e2e/metamask-ui.spec.js
  64. 94
      test/e2e/tests/add-hide-token.spec.js
  65. 90
      test/e2e/tests/custom-rpc-history.spec.js
  66. 5
      test/stub/tx-meta-stub.js
  67. 1
      ui/components/app/app-components.scss
  68. 2
      ui/components/app/asset-list-item/asset-list-item.js
  69. 2
      ui/components/app/asset-list/asset-list.js
  70. 2
      ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
  71. 2
      ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
  72. 5
      ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js
  73. 8
      ui/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js
  74. 1
      ui/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js
  75. 3
      ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js
  76. 14
      ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js
  77. 34
      ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  78. 7
      ui/components/app/loading-network-screen/loading-network-screen.component.js
  79. 41
      ui/components/app/menu-bar/account-options-menu.js
  80. 25
      ui/components/app/menu-bar/menu-bar.test.js
  81. 18
      ui/components/app/modals/account-details-modal/account-details-modal.component.js
  82. 1
      ui/components/app/modals/account-details-modal/account-details-modal.test.js
  83. 31
      ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js
  84. 2
      ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js
  85. 5
      ui/components/app/modals/qr-scanner/qr-scanner.component.js
  86. 1
      ui/components/app/recovery-phrase-reminder/index.js
  87. 10
      ui/components/app/recovery-phrase-reminder/index.scss
  88. 91
      ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js
  89. 3
      ui/components/app/selected-account/selected-account.component.js
  90. 5
      ui/components/app/sidebars/sidebar.component.js
  91. 22
      ui/components/app/transaction-activity-log/transaction-activity-log.component.js
  92. 2
      ui/components/app/transaction-activity-log/transaction-activity-log.container.js
  93. 15
      ui/components/app/transaction-activity-log/transaction-activity-log.util.test.js
  94. 3
      ui/components/app/transaction-breakdown/transaction-breakdown.component.test.js
  95. 7
      ui/components/app/transaction-breakdown/transaction-breakdown.container.js
  96. 16
      ui/components/app/transaction-icon/transaction-icon.js
  97. 32
      ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js
  98. 9
      ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js
  99. 4
      ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
  100. 2
      ui/components/app/wallet-overview/token-overview.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -6,10 +6,17 @@ set -o pipefail
CHROME_VERSION='79.0.3945.117-1'
CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
CHROME_BINARY_URL="http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}"
CHROME_BINARY_URL="http://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/${CHROME_BINARY}"
CHROME_BINARY_SHA512SUM='2d4f76202219a40e560477d79023fa4a847187a086278924a9d916dcd5fbefafdcf7dfd8879fae907b8276b244e71a3b8a1b00a88dee87b18738ce31134a6713'
wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}"
if [[ $(shasum -a 512 "${CHROME_BINARY}" | cut '--delimiter= ' -f1) != "${CHROME_BINARY_SHA512SUM}" ]]
then
exit 1
fi
(sudo dpkg -i "${CHROME_BINARY}" || sudo apt-get -fy install)
rm -rf "${CHROME_BINARY}"

@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Request a new feature
url: https://metamask.zendesk.com/hc/en-us/community/topics/360000682552-Feature-Requests
url: https://community.metamask.io/c/feature-requests-ideas/
about: Request new features and vote on the ones that are important to you
- name: Get support or ask a question
url: https://metamask.zendesk.com/hc/en-us/requests/new

@ -9,6 +9,9 @@ jobs:
CLABot:
if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/')
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: "CLA Signature Bot"
uses: MetaMask/cla-signature-bot@v3.0.2

@ -0,0 +1,56 @@
export const currentNetworkTxListSample = {
"id": 7900715443136469,
"time": 1621395091737,
"status": "unapproved",
"metamaskNetworkId": "1337",
"chainId": "0x539",
"loadingDefaults": false,
"txParams": {
"from": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"to": "0x057ef64e23666f000b34ae31332854acbd1c8544",
"value": "0x0",
"data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170",
"gas": "0xea60",
"gasPrice": "0x4a817c800"
},
"origin": "https://metamask.github.io",
"type": "approve",
"history": [
{
"id": 7900715443136469,
"time": 1621395091737,
"status": "unapproved",
"metamaskNetworkId": "1337",
"chainId": "0x539",
"loadingDefaults": true,
"txParams": {
"from": "0x90f79bf6eb2c4f870365e785982e1f101e93b906",
"to": "0x057ef64e23666f000b34ae31332854acbd1c8544",
"value": "0x0",
"data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170",
"gas": "0xea60",
"gasPrice": "0x4a817c800"
},
"origin": "https://metamask.github.io",
"type": "approve"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1621395091742
}
]
]
}
export const domainMetadata = {
"https://metamask.github.io": {
"name": "E2E Test Dapp",
"icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
"lastUpdated": 1620723443380,
"host": "metamask.github.io"
}
}

@ -0,0 +1,24 @@
import React from 'react';
import {
MetaMetricsProvider,
LegacyMetaMetricsProvider,
} from '../ui/contexts/metametrics';
import {
MetaMetricsProvider as NewMetaMetricsProvider,
LegacyMetaMetricsProvider as NewLegacyMetaMetricsProvider,
} from '../ui/contexts/metametrics.new';
const MetaMetricsProviderStorybook = (props) =>
(
<MetaMetricsProvider>
<LegacyMetaMetricsProvider>
<NewMetaMetricsProvider>
<NewLegacyMetaMetricsProvider>
{props.children}
</NewLegacyMetaMetricsProvider>
</NewMetaMetricsProvider>
</LegacyMetaMetricsProvider>
</MetaMetricsProvider>
);
export default MetaMetricsProviderStorybook

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { addDecorator, addParameters } from '@storybook/react';
import { useGlobals } from '@storybook/api';
import { action } from '@storybook/addon-actions';
import { withKnobs } from '@storybook/addon-knobs';
import { Provider } from 'react-redux';
import configureStore from '../ui/store/store';
@ -8,7 +8,11 @@ import '../ui/css/index.scss';
import localeList from '../app/_locales/index.json';
import * as allLocales from './locales';
import { I18nProvider, LegacyI18nProvider } from './i18n';
import MetaMetricsProviderStorybook from './metametrics'
import testData from './test-data.js';
import { Router } from "react-router-dom";
import { createBrowserHistory } from "history";
import { _setBackgroundConnection } from '../ui/store/actions'
addParameters({
backgrounds: {
@ -41,22 +45,36 @@ const styles = {
alignItems: 'center',
};
const store = configureStore(testData);
export const store = configureStore(testData);
const history = createBrowserHistory();
const proxiedBackground = new Proxy({}, {
get(_, method) {
return function() {
action(`Background call: ${method}`)()
return new Promise(() => {})
}
}
})
_setBackgroundConnection(proxiedBackground)
const metamaskDecorator = (story, context) => {
const currentLocale = context.globals.locale;
const current = allLocales[currentLocale];
return (
<Provider store={store}>
<I18nProvider
currentLocale={currentLocale}
current={current}
en={allLocales.en}
>
<LegacyI18nProvider>
<div style={styles}>{story()}</div>
</LegacyI18nProvider>
</I18nProvider>
<Router history={history}>
<MetaMetricsProviderStorybook>
<I18nProvider
currentLocale={currentLocale}
current={current}
en={allLocales.en}
>
<LegacyI18nProvider>
<div style={styles}>{story()}</div>
</LegacyI18nProvider>
</I18nProvider>
</MetaMetricsProviderStorybook>
</Router>
</Provider>
);
};

@ -1,217 +1,782 @@
import { TRANSACTION_STATUSES } from '../shared/constants/transaction';
const state = {
metamask: {
isInitialized: true,
isUnlocked: true,
featureFlags: { sendHexData: true },
identities: {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
name: 'Send Account 1',
},
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
name: 'Send Account 2',
},
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
name: 'Send Account 3',
},
'0xd85a4b6a394794842887b8284293d69163007bbb': {
address: '0xd85a4b6a394794842887b8284293d69163007bbb',
name: 'Send Account 4',
},
},
cachedBalances: {},
currentBlockGasLimit: '0x4c1878',
currentCurrency: 'USD',
conversionRate: 1200.88200327,
conversionDate: 1489013762,
nativeCurrency: 'ETH',
frequentRpcList: [],
network: '3',
provider: {
type: 'ropsten',
chainId: '0x3',
},
accounts: {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
code: '0x',
balance: '0x47c9d71831c76efe',
nonce: '0x1b',
address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
},
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
code: '0x',
balance: '0x37452b1315889f80',
nonce: '0xa',
address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
},
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
code: '0x',
balance: '0x30c9d71831c76efe',
nonce: '0x1c',
address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
},
'0xd85a4b6a394794842887b8284293d69163007bbb': {
code: '0x',
balance: '0x0',
nonce: '0x0',
address: '0xd85a4b6a394794842887b8284293d69163007bbb',
},
},
addressBook: {
'0x3': {
'0x06195827297c7a80a443b6894d3bdb8824b43896': {
address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
name: 'Address Book Account 1',
chainId: '0x3',
},
},
},
tokens: [
{
address: '0x1a195821297c7a80a433b6894d3bdb8824b43896',
decimals: 18,
symbol: 'ABC',
"invalidCustomNetwork": {
"state": "CLOSED",
"networkName": ""
},
"unconnectedAccount": {
"state": "CLOSED"
},
"activeTab": {},
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isAccountMenuOpen": false,
"rpcUrl": "https://rawtestrpc.metamask.io/",
"identities": {
"0x983211ce699ea5ab57cc528086154b6db1ad8e55": {
"name": "Account 1",
"address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55"
},
{
address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
decimals: 4,
symbol: 'DEF',
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {
"name": "Account 2",
"address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e"
},
"0x9d0ba4ddac06032527b140912ec808ab9451b788": {
"name": "Account 3",
"address": "0x9d0ba4ddac06032527b140912ec808ab9451b788"
}
},
"unapprovedTxs": {
"7786962153682822": {
"id": 7786962153682822,
"time": 1620710815484,
"status": "unapproved",
"metamaskNetworkId": "3",
"chainId": "0x3",
"loadingDefaults": false,
"txParams": {
"from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
"to": "0xad6d458402f60fd3bd25163575031acdce07538d",
"value": "0x0",
"data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000",
"gas": "0xcb28",
"gasPrice": "0x77359400"
},
"type": "standard",
"origin": "metamask",
"transactionCategory": "transfer",
"history": [
{
"id": 7786962153682822,
"time": 1620710815484,
"status": "unapproved",
"metamaskNetworkId": "3",
"chainId": "0x3",
"loadingDefaults": true,
"txParams": {
"from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
"to": "0xad6d458402f60fd3bd25163575031acdce07538d",
"value": "0x0",
"data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000",
"gas": "0xcb28",
"gasPrice": "0x77359400"
},
"type": "standard",
"origin": "metamask",
"transactionCategory": "transfer"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1620710815497
}
]
]
}
},
"frequentRpcList": [],
"addressBook": {
"undefined": {
"0": {
"address": "0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0",
"name": "",
"isEns": false
}
}
},
"contractExchangeRates": {
"0xad6d458402f60fd3bd25163575031acdce07538d": 0
},
"tokens": [
{
address: '0xa42084c8d1d9a2198631988579bb36b48433a72b',
decimals: 18,
symbol: 'GHI',
"address": "0xad6d458402f60fd3bd25163575031acdce07538d",
"symbol": "DAI",
"decimals": 18
}
],
"pendingTokens": {},
"customNonceValue": "",
"send": {
"gasLimit": "0xcb28",
"gasPrice": null,
"gasTotal": null,
"tokenBalance": "8.7a73149c048545a3fe58",
"from": "",
"to": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e",
"amount": "3782dace9d900000",
"memo": "",
"errors": {},
"maxModeOn": false,
"editingTransactionId": null,
"toNickname": "Account 2",
"ensResolution": null,
"ensResolutionError": "",
"token": {
"address": "0xad6d458402f60fd3bd25163575031acdce07538d",
"symbol": "DAI",
"decimals": 18
}
},
"useBlockie": false,
"featureFlags": {},
"welcomeScreenSeen": false,
"currentLocale": "en",
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
},
"firstTimeFlowType": "create",
"completedOnboarding": true,
"knownMethodData": {
"0x60806040": {
"name": "Approve Tokens"
},
"0x095ea7b3": {
"name": "Approve Tokens"
}
},
"participateInMetaMetrics": true,
"metaMetricsSendCount": 2,
"nextNonce": 71,
"connectedStatusPopoverHasBeenShown": true,
"swapsWelcomeMessageHasBeenShown": true,
"defaultHomeActiveTabName": "Assets",
"provider": {
"type": "ropsten",
"ticker": "ETH",
"nickname": "",
"rpcUrl": "",
"chainId": "0x3"
},
"previousProviderStore": {
"type": "ropsten",
"ticker": "ETH",
"nickname": "",
"rpcUrl": "",
"chainId": "0x3"
},
"network": "3",
"accounts": {
"0x983211ce699ea5ab57cc528086154b6db1ad8e55": {
"address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55",
"balance": "0x176e5b6f173ebe66"
},
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {
"address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e",
"balance": "0x2d3142f5000"
},
"0x9d0ba4ddac06032527b140912ec808ab9451b788": {
"address": "0x9d0ba4ddac06032527b140912ec808ab9451b788",
"balance": "0x15f6f0b9d4f8d000"
}
},
"currentBlockGasLimit": "0x793af4",
"currentNetworkTxList": [
],
"cachedBalances": {
"1": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x0",
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0xcaf5317161f400",
"0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x0"
},
"3": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x18d289d450bace66",
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0x2d3142f5000",
"0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x15f6f0b9d4f8d000"
},
"0x3": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x176e5b6f173ebe66",
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0x2d3142f5000",
"0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x15f6f0b9d4f8d000"
}
},
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"unapprovedDecryptMsgs": {},
"unapprovedDecryptMsgCount": 0,
"unapprovedEncryptionPublicKeyMsgs": {},
"unapprovedEncryptionPublicKeyMsgCount": 0,
"unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree",
"Trezor Hardware",
"Ledger Hardware"
],
transactions: {},
currentNetworkTxList: [
"keyrings": [
{
id: 'mockTokenTx1',
txParams: {
to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
from: '0xd85a4b6a394794842887b8284293d69163007bbb',
"type": "HD Key Tree",
"accounts": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e",
"0x9d0ba4ddac06032527b140912ec808ab9451b788"
]
}
],
"frequentRpcListDetail": [
{
"rpcUrl": "http://localhost:8545",
"chainId": "0x539",
"ticker": "ETH",
"nickname": "Localhost 8545",
"rpcPrefs": {}
}
],
"accountTokens": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": {
"0x1": [
{
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
"symbol": "DAI",
"decimals": 18
},
{
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
"symbol": "BAT",
"decimals": 18
}
],
"0x3": [
{
"address": "0xad6d458402f60fd3bd25163575031acdce07538d",
"symbol": "DAI",
"decimals": 18
}
]
},
"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {},
"0x9d0ba4ddac06032527b140912ec808ab9451b788": {}
},
"accountHiddenTokens": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": {
"0x3": []
}
},
"assetImages": {
"0xad6d458402f60fd3bd25163575031acdce07538d": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xaD6D458402F60fD3Bd25163575031ACDce07538D/logo.png"
},
"hiddenTokens": [],
"suggestedTokens": {},
"useNonceField": false,
"usePhishDetect": true,
"lostIdentities": {},
"forgottenPassword": false,
"ipfsGateway": "dweb.link",
"infuraBlocked": false,
"migratedPrivacyMode": false,
"selectedAddress": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
"metaMetricsId": "0xc2377d11fec1c3b7dd88c4854240ee5e3ed0d9f63b00456d98d80320337b827f",
"conversionDate": 1620710825.03,
"conversionRate": 3910.28,
"currentCurrency": "usd",
"nativeCurrency": "ETH",
"usdConversionRate": 3910.28,
"ticker": "ETH",
"alertEnabledness": {
"unconnectedAccount": true,
"web3ShimUsage": true
},
"unconnectedAccountAlertShownOrigins": {},
"web3ShimUsageOrigins": {},
"seedPhraseBackedUp": null,
"onboardingTabs": {},
"incomingTransactions": {
"0x2de9256a7c604586f7ecfd87ae9509851e217f588f9f85feed793c54ed2ce0aa": {
"blockNumber": "8888976",
"id": 4678200543090532,
"metamaskNetworkId": "1",
"status": "confirmed",
"time": 1573114896000,
"txParams": {
"from": "0x3f1b52850109023775d238c7ed5d5e7161041fd1",
"gas": "0x5208",
"gasPrice": "0x124101100",
"nonce": "0x35",
"to": "0x045c619e4d29bba3b92769508831b681b83d6a96",
"value": "0xbca9bce4d98ca3"
},
time: 1700000000000,
"hash": "0x2de9256a7c604586f7ecfd87ae9509851e217f588f9f85feed793c54ed2ce0aa",
"transactionCategory": "incoming"
},
"0x320a1fd769373578f78570e5d8f56e89bc7bce9657bb5f4c12d8fe790d471bfd": {
"blockNumber": "9453174",
"id": 4678200543090535,
"metamaskNetworkId": "1",
"status": "confirmed",
"time": 1581312411000,
"txParams": {
"from": "0xa17bd07d6d38cb9e37b29f7659a4b1047701e969",
"gas": "0xc350",
"gasPrice": "0x1a13b8600",
"nonce": "0x0",
"to": "0x045c619e4d29bba3b92769508831b681b83d6a96",
"value": "0xcdb08ab4254000"
},
"hash": "0x320a1fd769373578f78570e5d8f56e89bc7bce9657bb5f4c12d8fe790d471bfd",
"transactionCategory": "incoming"
},
"0x8add6c1ea089a8de9b15fa2056b1875360f17916755c88ace9e5092b7a4b1239": {
"blockNumber": "10892417",
"id": 4678200543090542,
"metamaskNetworkId": "1",
"status": "confirmed",
"time": 1600515224000,
"txParams": {
"from": "0x0681d8db095565fe8a346fa0277bffde9c0edbbf",
"gas": "0x5208",
"gasPrice": "0x1d1a94a200",
"nonce": "0x2bb8a5",
"to": "0x045c619e4d29bba3b92769508831b681b83d6a96",
"value": "0xe6ed27d6668000"
},
"hash": "0x8add6c1ea089a8de9b15fa2056b1875360f17916755c88ace9e5092b7a4b1239",
"transactionCategory": "incoming"
},
"0x50be62ab1cabd03ff104c602c11fdef7a50f3d73c55006d5583ba97950ab1144": {
"blockNumber": "10902987",
"id": 4678200543090545,
"metamaskNetworkId": "1",
"status": "confirmed",
"time": 1600654021000,
"txParams": {
"from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
"gas": "0x5208",
"gasPrice": "0x147d357000",
"nonce": "0xf",
"to": "0x045c619e4d29bba3b92769508831b681b83d6a96",
"value": "0x63eb89da4ed00000"
},
"hash": "0x50be62ab1cabd03ff104c602c11fdef7a50f3d73c55006d5583ba97950ab1144",
"transactionCategory": "incoming"
}
},
"incomingTxLastFetchedBlocksByNetwork": {
"ropsten": 8872820,
"rinkeby": null,
"kovan": null,
"goerli": null,
"mainnet": 10902989
},
"permissionsRequests": [],
"permissionsDescriptions": {},
"domains": {
"https://app.uniswap.org": {
"permissions": [
{
"@context": [
"https://github.com/MetaMask/rpc-cap"
],
"invoker": "https://app.uniswap.org",
"parentCapability": "eth_accounts",
"id": "a7342e4b-beae-4525-a36c-c0635fd03359",
"date": 1620710693178,
"caveats": [
{
"type": "limitResponseLength",
"value": 1,
"name": "primaryAccountOnly"
},
{
"type": "filterResponse",
"value": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4"
],
"name": "exposedAccounts"
}
]
}
]
}
},
"permissionsLog": [
{
id: 'mockTokenTx2',
txParams: {
to: '0xafaketokenaddress',
from: '0xd85a4b6a394794842887b8284293d69163007bbb',
"id": 522690215,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "https://metamask.io",
"request": {
"method": "eth_accounts",
"params": [],
"jsonrpc": "2.0",
"id": 522690215,
"origin": "https://metamask.io",
"tabId": 5
},
time: 1600000000000,
"requestTime": 1602643170686,
"response": {
"id": 522690215,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1602643170688,
"success": true
},
{
id: 'mockTokenTx3',
txParams: {
to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
from: '0xd85a4b6a394794842887b8284293d69163007bbb',
"id": 1620464600,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "https://widget.getacute.io",
"request": {
"method": "eth_accounts",
"params": [],
"jsonrpc": "2.0",
"id": 1620464600,
"origin": "https://widget.getacute.io",
"tabId": 5
},
"requestTime": 1602643172935,
"response": {
"id": 1620464600,
"jsonrpc": "2.0",
"result": []
},
time: 1500000000000,
"responseTime": 1602643172935,
"success": true
},
{
id: 'mockEthTx1',
txParams: {
to: '0xd85a4b6a394794842887b8284293d69163007bbb',
from: '0xd85a4b6a394794842887b8284293d69163007bbb',
"id": 4279100021,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "https://app.uniswap.org",
"request": {
"method": "eth_accounts",
"jsonrpc": "2.0",
"id": 4279100021,
"origin": "https://app.uniswap.org",
"tabId": 5
},
time: 1400000000000,
"requestTime": 1620710669962,
"response": {
"id": 4279100021,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1620710669963,
"success": true
},
],
unapprovedMsgs: {
'0xabc': { id: 'unapprovedMessage1', time: 1650000000000 },
'0xdef': { id: 'unapprovedMessage2', time: 1550000000000 },
'0xghi': { id: 'unapprovedMessage3', time: 1450000000000 },
},
unapprovedMsgCount: 0,
unapprovedPersonalMsgs: {},
unapprovedPersonalMsgCount: 0,
unapprovedDecryptMsgs: {},
unapprovedDecryptMsgCount: 0,
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
keyringTypes: ['Simple Key Pair', 'HD Key Tree'],
keyrings: [
{
type: 'HD Key Tree',
accounts: [
'fdea65c8e26263f6d9a1b5de9555d2931a33b825',
'c5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'2f8d4a878cfa04a6e60d46362f5644deab66572d',
],
"id": 4279100022,
"method": "eth_requestAccounts",
"methodType": "restricted",
"origin": "https://app.uniswap.org",
"request": {
"method": "eth_requestAccounts",
"jsonrpc": "2.0",
"id": 4279100022,
"origin": "https://app.uniswap.org",
"tabId": 5
},
"requestTime": 1620710686872,
"response": {
"id": 4279100022,
"jsonrpc": "2.0",
"result": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4"
]
},
"responseTime": 1620710693187,
"success": true
},
{
type: 'Simple Key Pair',
accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'],
"id": 4279100023,
"method": "eth_requestAccounts",
"methodType": "restricted",
"origin": "https://app.uniswap.org",
"request": {
"method": "eth_requestAccounts",
"jsonrpc": "2.0",
"id": 4279100023,
"origin": "https://app.uniswap.org",
"tabId": 5
},
"requestTime": 1620710693204,
"response": {
"id": 4279100023,
"jsonrpc": "2.0",
"result": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4"
]
},
"responseTime": 1620710693213,
"success": true
},
{
"id": 4279100034,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "https://app.uniswap.org",
"request": {
"method": "eth_accounts",
"params": [],
"jsonrpc": "2.0",
"id": 4279100034,
"origin": "https://app.uniswap.org",
"tabId": 5
},
"requestTime": 1620710712072,
"response": {
"id": 4279100034,
"jsonrpc": "2.0",
"result": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4"
]
},
"responseTime": 1620710712075,
"success": true
}
],
selectedAddress: '0xd85a4b6a394794842887b8284293d69163007bbb',
send: {
gasLimit: '0xFFFF',
gasPrice: '0xaa',
gasTotal: '0xb451dc41b578',
tokenBalance: 3434,
from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
to: '0x987fedabc',
amount: '0x080',
memo: '',
errors: {
someError: null,
},
maxModeOn: false,
editingTransactionId: 97531,
},
unapprovedTxs: {
4768706228115573: {
id: 4768706228115573,
time: 1487363153561,
status: TRANSACTION_STATUSES.UNAPPROVED,
gasMultiplier: 1,
metamaskNetworkId: '3',
txParams: {
from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
value: '0xde0b6b3a7640000',
metamaskId: 4768706228115573,
metamaskNetworkId: '3',
gas: '0x5209',
},
txFee: '17e0186e60800',
txValue: 'de0b6b3a7640000',
maxCost: 'de234b52e4a0800',
gasPrice: '4a817c800',
},
},
currentLocale: 'en',
"permissionsHistory": {
"https://app.uniswap.org": {
"eth_accounts": {
"lastApproved": 1620710693213,
"accounts": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": 1620710693213
}
}
}
},
"domainMetadata": {
"https://metamask.github.io": {
"name": "E2E Test Dapp",
"icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
"lastUpdated": 1620723443380,
"host": "metamask.github.io"
}
},
"threeBoxSyncingAllowed": false,
"showRestorePrompt": true,
"threeBoxLastUpdated": 0,
"threeBoxAddress": null,
"threeBoxSynced": false,
"threeBoxDisabled": false,
"swapsState": {
"quotes": {},
"fetchParams": null,
"tokens": null,
"tradeTxId": null,
"approveTxId": null,
"quotesLastFetched": null,
"customMaxGas": "",
"customGasPrice": null,
"selectedAggId": null,
"customApproveTxData": "",
"errorKey": "",
"topAggId": null,
"routeState": "",
"swapsFeatureIsLive": false,
"swapsQuoteRefreshTime": 60000
},
"ensResolutionsByAddress": {},
"pendingApprovals": {},
"pendingApprovalCount": 0
},
appState: {
menuOpen: false,
currentView: {
name: 'accountDetail',
detailView: null,
context: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
},
accountDetail: {
subview: 'transactions',
},
modal: {
modalState: {},
previousModalState: {},
},
isLoading: false,
warning: null,
scrollToBottom: false,
forgottenPassword: null,
"appState": {
"shouldClose": false,
"menuOpen": false,
"modal": {
"open": false,
"modalState": {
"name": null,
"props": {}
},
"previousModalState": {
"name": null
}
},
"sidebar": {
"isOpen": false,
"transitionName": "",
"type": "",
"props": {}
},
"alertOpen": false,
"alertMessage": null,
"qrCodeData": null,
"networkDropdownOpen": false,
"accountDetail": {
"subview": "transactions"
},
"isLoading": false,
"warning": null,
"buyView": {},
"isMouseUser": true,
"gasIsLoading": false,
"defaultHdPaths": {
"trezor": "m/44'/60'/0'/0",
"ledger": "m/44'/60'/0'/0/0"
},
"networksTabSelectedRpcUrl": "",
"networksTabIsInAddMode": false,
"loadingMethodData": false,
"show3BoxModalAfterImport": false,
"threeBoxLastUpdated": null,
"requestAccountTabs": {},
"openMetaMaskTabs": {},
"currentWindowTab": {}
},
send: {
fromDropdownOpen: false,
toDropdownOpen: false,
errors: { someError: null },
"history": {
"mostRecentOverviewPage": "/"
},
};
"send": {
"toDropdownOpen": false,
"gasButtonGroupShown": true,
"errors": {}
},
"confirmTransaction": {
"txData": {
"id": 3111025347726181,
"time": 1620723786838,
"status": "unapproved",
"metamaskNetworkId": "3",
"chainId": "0x3",
"loadingDefaults": false,
"txParams": {
"from": "0x983211ce699ea5ab57cc528086154b6db1ad8e55",
"to": "0xad6d458402f60fd3bd25163575031acdce07538d",
"value": "0x0",
"data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170",
"gas": "0xea60",
"gasPrice": "0x4a817c800"
},
"type": "standard",
"origin": "https://metamask.github.io",
"transactionCategory": "approve",
"history": [
{
"id": 3111025347726181,
"time": 1620723786838,
"status": "unapproved",
"metamaskNetworkId": "3",
"chainId": "0x3",
"loadingDefaults": true,
"txParams": {
"from": "0x983211ce699ea5ab57cc528086154b6db1ad8e55",
"to": "0xad6d458402f60fd3bd25163575031acdce07538d",
"value": "0x0",
"data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170",
"gas": "0xea60",
"gasPrice": "0x4a817c800"
},
"type": "standard",
"origin": "https://metamask.github.io",
"transactionCategory": "approve"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1620723786844
}
]
]
},
"tokenData": {
"args": [
"0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4",
{
"type": "BigNumber",
"hex": "0x011170"
}
],
"functionFragment": {
"type": "function",
"name": "approve",
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address",
"indexed": null,
"components": null,
"arrayLength": null,
"arrayChildren": null,
"baseType": "address",
"_isParamType": true
},
{
"name": "_value",
"type": "uint256",
"indexed": null,
"components": null,
"arrayLength": null,
"arrayChildren": null,
"baseType": "uint256",
"_isParamType": true
}
],
"outputs": [
{
"name": "success",
"type": "bool",
"indexed": null,
"components": null,
"arrayLength": null,
"arrayChildren": null,
"baseType": "bool",
"_isParamType": true
}
],
"payable": false,
"stateMutability": "nonpayable",
"gas": null,
"_isFragment": true
},
"name": "approve",
"signature": "approve(address,uint256)",
"sighash": "0x095ea7b3",
"value": {
"type": "BigNumber",
"hex": "0x00"
}
},
"fiatTransactionAmount": "0",
"fiatTransactionFee": "4.72",
"fiatTransactionTotal": "4.72",
"ethTransactionAmount": "0",
"ethTransactionFee": "0.0012",
"ethTransactionTotal": "0.0012",
"hexTransactionAmount": "0x0",
"hexTransactionFee": "0x44364c5bb0000",
"hexTransactionTotal": "0x44364c5bb0000",
"nonce": ""
},
"swaps": {
"aggregatorMetadata": null,
"approveTxId": null,
"balanceError": false,
"fetchingQuotes": false,
"fromToken": null,
"quotesFetchStartTime": null,
"topAssets": {},
"toToken": null,
"customGas": {
"price": null,
"limit": null,
"loading": "INITIAL",
"priceEstimates": {},
"fallBackPrice": null
}
},
"gas": {
"customData": {
"price": null,
"limit": "0xcb28"
},
"basicEstimates": {
"average": 2
},
"basicEstimateIsLoading": false
}
}
export default state;

@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [9.7.0]
### Added
- [#11021](https://github.com/MetaMask/metamask-extension/pull/11021): Add periodic reminder modal for backing up recovery phrase
- [#11179](https://github.com/MetaMask/metamask-extension/pull/11179): Add warning to the custom network form when attempting to add a custom network that already exists
### Changed
- [#11200](https://github.com/MetaMask/metamask-extension/pull/11200): Swaps: Shows custom tokens added from main assets tab
- [#11111](https://github.com/MetaMask/metamask-extension/pull/11111): Removing low gas price warning in advanced tab on test networks
- [#11145](https://github.com/MetaMask/metamask-extension/pull/11145): Swaps: Improving price difference notifications and warnings
- [#11124](https://github.com/MetaMask/metamask-extension/pull/11124): Swaps: Allowing for continual new swap submissions without flow reset
- [#11278](https://github.com/MetaMask/metamask-extension/pull/11278): Updated contract-metadata version to 1.26.0
### Fixed
- [#11017](https://github.com/MetaMask/metamask-extension/pull/11017): Fixes custom RPC block explorer links
- [#11257](https://github.com/MetaMask/metamask-extension/pull/11257): Fixes incorrect network currency label in encryption public key requests
## [9.6.1]
### Fixed
- [#11309](https://github.com/MetaMask/metamask-extension/pull/11309): Fixed signTypeData parameter validation issue
@ -2301,7 +2317,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.7.0...HEAD
[9.7.0]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...v9.7.0
[9.6.1]: https://github.com/MetaMask/metamask-extension/compare/v9.6.0...v9.6.1
[9.6.0]: https://github.com/MetaMask/metamask-extension/compare/v9.5.9...v9.6.0
[9.5.9]: https://github.com/MetaMask/metamask-extension/compare/v9.5.8...v9.5.9

@ -52,6 +52,10 @@
"addContact": {
"message": "Add contact"
},
"addCustomTokenByContractAddress": {
"message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1.",
"description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum"
},
"addEthereumChainConfirmationDescription": {
"message": "This will allow this network to be used within MetaMask."
},
@ -285,6 +289,9 @@
"chainIdDefinition": {
"message": "The chain ID used to sign transactions for this network."
},
"chainIdExistsErrorMsg": {
"message": "This Chain ID is currently used by the $1 network."
},
"chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
},
@ -410,6 +417,9 @@
"continueToWyre": {
"message": "Continue to Wyre"
},
"contract": {
"message": "Contract"
},
"contractAddressError": {
"message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens."
},
@ -893,6 +903,12 @@
"message": "or $1",
"description": "$1 represents the text from `importAccountLinkText` as a link"
},
"importTokenQuestion": {
"message": "Import token?"
},
"importTokenWarning": {
"message": "Anyone can create a token with any name, including fake versions of existing tokens. Add and trade at your own risk!"
},
"importWallet": {
"message": "Import wallet"
},
@ -1042,6 +1058,9 @@
"mainnet": {
"message": "Ethereum Mainnet"
},
"makeAnotherSwap": {
"message": "Create a new swap"
},
"max": {
"message": "Max"
},
@ -1438,6 +1457,30 @@
"recipientAddressPlaceholder": {
"message": "Search, public address (0x), or ENS"
},
"recoveryPhraseReminderBackupStart": {
"message": "Start here"
},
"recoveryPhraseReminderConfirm": {
"message": "Got it"
},
"recoveryPhraseReminderHasBackedUp": {
"message": "Always keep your Secret Recovery Phrase in a secure and secret place"
},
"recoveryPhraseReminderHasNotBackedUp": {
"message": "Need to backup your Secret Recovery Phrase again?"
},
"recoveryPhraseReminderItemOne": {
"message": "Never share your Secret Recovery Phrase with anyone"
},
"recoveryPhraseReminderItemTwo": {
"message": "The MetaMask team will never ask for your Secret Recovery Phrase"
},
"recoveryPhraseReminderSubText": {
"message": "Your Secret Recovery Phrase controls all of your accounts."
},
"recoveryPhraseReminderTitle": {
"message": "Protect your funds"
},
"reject": {
"message": "Reject"
},
@ -1949,18 +1992,18 @@
"message": "You are about to swap $1 $2 (~$3) for $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."
},
"swapPriceDifferenceAcknowledgement": {
"message": "I'm aware"
},
"swapPriceDifferenceTitle": {
"message": "Price difference of ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "The difference in market prices can be affected by fees taken by intermediaries, size of market, size of trade, or market inefficiencies."
"swapPriceImpactTooltip": {
"message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool."
},
"swapPriceUnavailableDescription": {
"message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping."
},
"swapPriceDifferenceUnavailable": {
"message": "Market price is unavailable. Make sure you feel comfortable with the returned amount before proceeding."
"swapPriceUnavailableTitle": {
"message": "Check your rate before proceeding"
},
"swapProcessing": {
"message": "Processing"
@ -2063,13 +2106,13 @@
"message": "Swap $1 to $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": "This token has been added manually."
},
"swapTokenVerificationMessage": {
"message": "Always confirm the token address on $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."
},
"swapTokenVerificationNoSource": {
"message": "This token has not been verified."
},
"swapTokenVerificationOnlyOneSource": {
"message": "Only verified on 1 source."
},
@ -2093,9 +2136,6 @@
"message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "View $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 available to swap",
"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"
@ -2219,6 +2259,9 @@
"tokenSymbol": {
"message": "Token Symbol"
},
"tooltipApproveButton": {
"message": "I understand"
},
"total": {
"message": "Total"
},
@ -2333,7 +2376,7 @@
"message": "URLs require the appropriate HTTP/HTTPS prefix."
},
"urlExistsErrorMsg": {
"message": "URL is already present in existing list of networks"
"message": "This URL is currently used by the $1 network."
},
"usePhishingDetection": {
"message": "Use Phishing Detection"
@ -2355,6 +2398,10 @@
"message": "Verify this token on $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": "Verify this token on $1 and make sure this is the token you want to 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": "View Account"
},

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "Red principal de Ethereum"
},
"makeAnotherSwap": {
"message": "Crear un nuevo canje"
},
"max": {
"message": "Máx."
},
@ -1893,12 +1896,6 @@
"message": "Diferencia de precio de ~$1 %",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "La diferencia en los precios de mercado puede verse afectada por las tarifas cobradas por los intermediarios, el tamaño del mercado, el tamaño del comercio o las ineficiencias del mercado."
},
"swapPriceDifferenceUnavailable": {
"message": "El precio de mercado no está disponible. Asegúrese de sentirse cómodo con el monto devuelto antes de continuar."
},
"swapProcessing": {
"message": "Procesamiento"
},
@ -2027,9 +2024,6 @@
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Ver $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 disponible para canje",
"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"
@ -2153,6 +2147,9 @@
"tokenSymbol": {
"message": "Símbolo del token"
},
"tooltipApproveButton": {
"message": "Comprendo"
},
"total": {
"message": "Total"
},

@ -1042,6 +1042,9 @@
"mainnet": {
"message": "Red principal de Ethereum"
},
"makeAnotherSwap": {
"message": "Crear un nuevo canje"
},
"max": {
"message": "Máx."
},
@ -1937,12 +1940,6 @@
"message": "Diferencia de precio de ~$1 %",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "La diferencia en los precios de mercado puede verse afectada por las tarifas cobradas por los intermediarios, el tamaño del mercado, el tamaño del comercio o las ineficiencias del mercado."
},
"swapPriceDifferenceUnavailable": {
"message": "El precio de mercado no está disponible. Asegúrese de sentirse cómodo con el monto devuelto antes de continuar."
},
"swapProcessing": {
"message": "Procesamiento"
},
@ -2071,9 +2068,6 @@
"message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Ver $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 disponible para canje",
"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"
@ -2197,6 +2191,9 @@
"tokenSymbol": {
"message": "Símbolo del token"
},
"tooltipApproveButton": {
"message": "Comprendo"
},
"total": {
"message": "Total"
},

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "Ethereum Mainnet"
},
"makeAnotherSwap": {
"message": "एक नयप बन"
},
"max": {
"message": "अधिकतम"
},
@ -1893,6 +1896,15 @@
"message": "~$1% कय अतर",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "मय परभव, वरतमन बर मय और लन-दन निदन कन पत रिच कतर ह। मय परभव चलनििल क आकर कष आपकर क आकर क एक कय ह।"
},
"swapPriceUnavailableDescription": {
"message": "बर मय ड कमरण मय परभव किित नहि सक। कपयि करि आप सप करन पहलत हकन किकर सहज ह।"
},
"swapPriceUnavailableTitle": {
"message": "आग बढ पहल अपन दर कच कर"
},
"swapProcessing": {
"message": "परसकरण"
},
@ -2021,9 +2033,6 @@
"message": "एकिक टकन एक हम और परतक क उपयग कर सकत। यह सतित करनिए $1 कच करि यह वहकन ह, जिसक आप तलश कर रह।",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 द"
},
"swapYourTokenBalance": {
"message": "$1 $2 सप किए उपलबध ह",
"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"
@ -2147,6 +2156,9 @@
"tokenSymbol": {
"message": "टकन करतक"
},
"tooltipApproveButton": {
"message": "म समझत"
},
"total": {
"message": "कलयग"
},

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "Ethereum Mainnet"
},
"makeAnotherSwap": {
"message": "Buat penukaran baru"
},
"max": {
"message": "Maks."
},
@ -1893,6 +1896,15 @@
"message": "Perbedaan harga ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "Dampak harga adalah selisih antara harga pasar saat ini dan jumlah yang diterima selama terjadinya transaksi. Dampak harga adalah fungsi ukuran dagang relatif terhadap ukuran pool likuiditas."
},
"swapPriceUnavailableDescription": {
"message": "Dampak harga tidak dapat ditentukan karena kurangnya data harga pasar. Harap konfirmasi bahwa Anda setuju dengan jumlah token yang akan Anda terima sebelum penukaran."
},
"swapPriceUnavailableTitle": {
"message": "Periksa tarif Anda sebelum melanjutkan"
},
"swapProcessing": {
"message": "Memproses"
},
@ -2021,9 +2033,6 @@
"message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Lihat $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 tersedia untuk ditukar",
"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"
@ -2147,6 +2156,9 @@
"tokenSymbol": {
"message": "Simbol Token"
},
"tooltipApproveButton": {
"message": "Saya paham"
},
"total": {
"message": "Total"
},

@ -1621,19 +1621,10 @@
"message": "Stai per scambiare $1 $2 (~$3) per $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."
},
"swapPriceDifferenceAcknowledgement": {
"message": "Sono consapevole"
},
"swapPriceDifferenceTitle": {
"message": "Differenza di prezzo di circa ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "La differenza tra i prezzi del mercato può essere influenzata da commissioni prelevate da intermediari, dimensione del mercato, dimensione dello scambio, o inefficienze del mercato."
},
"swapPriceDifferenceUnavailable": {
"message": "Il prezzo di mercato non è disponibile. Assicurati di sentirti a tuo agio con l'importo restituito prima di procedere."
},
"swapProcessing": {
"message": "In elaborazione"
},
@ -1749,9 +1740,6 @@
"message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Vedi $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 disponibili allo scambio",
"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"

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "イーサリアム メインネット"
},
"makeAnotherSwap": {
"message": "新しいスワップの作成"
},
"max": {
"message": "最大"
},
@ -1893,12 +1896,6 @@
"message": "約 $1% の価格差",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "市場価格の違いは、仲介業者が負担する手数料、市場規模、取引量、または取引価格差の影響を受けることがあります。"
},
"swapPriceDifferenceUnavailable": {
"message": "マーケット価格は利用できません。続行する前に、返金額に問題がないことを確認してください。"
},
"swapProcessing": {
"message": "処理中"
},
@ -2027,9 +2024,6 @@
"message": "複数のトークンが同じ名前とシンボルを使用できます。$1 をチェックして、これが探しているトークンであることを確認します。",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 を表示"
},
"swapYourTokenBalance": {
"message": "$1 $2 はスワップに使用可能です",
"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"
@ -2153,6 +2147,9 @@
"tokenSymbol": {
"message": "トークン シンボル"
},
"tooltipApproveButton": {
"message": "理解しました"
},
"total": {
"message": "合計"
},

@ -1038,6 +1038,9 @@
"mainnet": {
"message": "이더리움 메인넷"
},
"makeAnotherSwap": {
"message": "새 스왑 생성"
},
"max": {
"message": "최대"
},
@ -1933,6 +1936,15 @@
"message": "~$1%의 가격 차이",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "가격 영향은 현재 시장 가격과 거래 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 거래의 크기를 나타내는 함수입니다."
},
"swapPriceUnavailableDescription": {
"message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수에 만족하시는지 확인하시기 바랍니다."
},
"swapPriceUnavailableTitle": {
"message": "진행하기 전에 요율을 확인하십시오."
},
"swapProcessing": {
"message": "처리 중"
},
@ -2061,9 +2073,6 @@
"message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1을(를) 확인하여 원하는 토큰인지 확인하세요.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "$1 보기"
},
"swapYourTokenBalance": {
"message": "$1 $2 스왑 가능",
"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"
@ -2187,6 +2196,9 @@
"tokenSymbol": {
"message": "토큰 기호"
},
"tooltipApproveButton": {
"message": "이해했습니다."
},
"total": {
"message": "합계"
},

@ -1042,6 +1042,9 @@
"mainnet": {
"message": "Ethereum Mainnet"
},
"makeAnotherSwap": {
"message": "Gumawa ng bagong swap"
},
"max": {
"message": "Max"
},
@ -1937,6 +1940,15 @@
"message": "Kaibahan sa presyo na ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "Ang epekto sa presyo ay ang pagkakaiba sa kasalukuyang presyo sa merkado at sa halagang natanggap sa pag-execute ng transaksyon. Ang epekto sa presyo ay isang function ng laki ng iyong trade kumpara sa laki ng liquidity pool."
},
"swapPriceUnavailableDescription": {
"message": "Hindi natukoy ang epekto sa presyo dahil sa kakulangan ng data sa presyo sa merkado. Pakikumpirma na kumportable ka sa dami ng mga token na matatanggap mo bago makipag-swap."
},
"swapPriceUnavailableTitle": {
"message": "Tingnan ang iyong rate bago magpatuloy"
},
"swapProcessing": {
"message": "Pagproseso"
},
@ -2188,6 +2200,9 @@
"tokenSymbol": {
"message": "Simbolo ng Token"
},
"tooltipApproveButton": {
"message": "Nauunawaan ko"
},
"total": {
"message": "Kabuuan"
},

@ -1028,6 +1028,9 @@
"mainnet": {
"message": "Mainnet do Ethereum"
},
"makeAnotherSwap": {
"message": "Criar novo swap"
},
"max": {
"message": "Máx"
},
@ -1877,6 +1880,15 @@
"message": "Diferença de preço de aproximadamente $1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "O impacto no preço é a diferença entre o preço de mercado atual e o valor recebido durante a execução da transação. O impacto no preço é uma função do tamanho do seu comércio em relação ao tamanho do pool de liquidez."
},
"swapPriceUnavailableDescription": {
"message": "O impacto no preço não poderia ser determinado devido aos dados do preço de mercado. Confirme que você está satisfeito com o valor dos tokens que você está prestes a receber antes de fazer swap."
},
"swapPriceUnavailableTitle": {
"message": "Verifique sua taxa antes de continuar"
},
"swapProcessing": {
"message": "Processando"
},
@ -2128,6 +2140,9 @@
"tokenSymbol": {
"message": "Símbolo do token"
},
"tooltipApproveButton": {
"message": "Eu entendo"
},
"total": {
"message": "Total"
},

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "Сеть Ethereum Mainnet"
},
"makeAnotherSwap": {
"message": "Создать новый своп"
},
"max": {
"message": "Макс."
},
@ -1893,6 +1896,15 @@
"message": "Разница в цене составляет ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время выполнения транзакции. Колебание цены зависит от размера вашей сделки относительно размера пула ликвидности."
},
"swapPriceUnavailableDescription": {
"message": "Колебание цены определить не удалось из-за отсутствия данных о рыночных ценах. Перед свопом подтвердите, что вас устраивает количество токенов, которое вы получите."
},
"swapPriceUnavailableTitle": {
"message": "Прежде чем продолжить, проверьте курс"
},
"swapProcessing": {
"message": "Обработка"
},
@ -2021,9 +2033,6 @@
"message": "Несколько токенов могут использовать одно и то же имя и символ. Убедитесь, что это именно тот токен, который вы ищете, на $1.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Просмотреть $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 доступны для свопа",
"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"
@ -2147,6 +2156,9 @@
"tokenSymbol": {
"message": "Символ токена"
},
"tooltipApproveButton": {
"message": "Я понимаю"
},
"total": {
"message": "Итого"
},

@ -1699,9 +1699,6 @@
"message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Tingnan ang $1"
},
"swapYourTokenBalance": {
"message": "Available ang $1 $2 na i-swap",
"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"

@ -1034,6 +1034,9 @@
"mainnet": {
"message": "Mạng chính thức của Ethereum"
},
"makeAnotherSwap": {
"message": "Tạo một giao dịch hoán đổi mới"
},
"max": {
"message": "Tối đa"
},
@ -1893,6 +1896,15 @@
"message": "Chênh lệch giá ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceImpactTooltip": {
"message": "Tác động về giá là mức chênh lệch giữa giá thị trường hiện tại và số tiền nhận được trong quá trình thực hiện giao dịch. Tác động giá là một hàm trong quy mô giao dịch của bạn so với quy mô của nhóm thanh khoản."
},
"swapPriceUnavailableDescription": {
"message": "Không thể xác định tác động giá do thiếu dữ liệu giá thị trường. Vui lòng xác nhận rằng bạn cảm thấy thoải mái với số lượng token bạn sắp nhận được trước khi hoán đổi."
},
"swapPriceUnavailableTitle": {
"message": "Hãy kiểm tra tỷ giá trước khi tiếp tục"
},
"swapProcessing": {
"message": "Đang xử lý"
},
@ -2021,9 +2033,6 @@
"message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "Xem $1"
},
"swapYourTokenBalance": {
"message": "Có sẵn $1 $2 để hoán đổi",
"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"
@ -2147,6 +2156,9 @@
"tokenSymbol": {
"message": "Ký hiệu token"
},
"tooltipApproveButton": {
"message": "Tôi đã hiểu"
},
"total": {
"message": "Tổng"
},

@ -1619,12 +1619,6 @@
"message": "价格差异 ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
},
"swapPriceDifferenceTooltip": {
"message": "市场价格的差异可能受到中介机构收取的费用、市场规模、交易规模或市场效率低下的影响。"
},
"swapPriceDifferenceUnavailable": {
"message": "市场价格不可用。 请确认您对退回的数额感到满意后再继续。"
},
"swapProcessing": {
"message": "处理中"
},
@ -1726,9 +1720,6 @@
"message": "多个代币可以使用相同的名称和符号。检查 $1(以太坊浏览器)以确认这是您正在寻找的代币。",
"description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network."
},
"swapViewToken": {
"message": "查看 $1"
},
"swapYourTokenBalance": {
"message": "$1 $2 可用",
"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"

@ -19,6 +19,7 @@ import {
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../shared/constants/app';
import { SECOND } from '../../shared/constants/time';
import migrations from './migrations';
import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension';
@ -491,7 +492,7 @@ async function openPopup() {
clearInterval(interval);
resolve();
}
}, 1000);
}, SECOND);
});
}

@ -1,6 +1,7 @@
import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import { MINUTE } from '../../../shared/constants/time';
export default class AppStateController extends EventEmitter {
/**
@ -24,6 +25,8 @@ export default class AppStateController extends EventEmitter {
connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: null,
browserEnvironment: {},
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(),
...initState,
});
this.timer = null;
@ -112,6 +115,27 @@ export default class AppStateController extends EventEmitter {
});
}
/**
* Record that the user has been shown the recovery phrase reminder
* @returns {void}
*/
setRecoveryPhraseReminderHasBeenShown() {
this.store.updateState({
recoveryPhraseReminderHasBeenShown: true,
});
}
/**
* Record the timestamp of the last time the user has seen the recovery phrase reminder
* @param {number} lastShown - timestamp when user was last shown the reminder
* @returns {void}
*/
setRecoveryPhraseReminderLastShown(lastShown) {
this.store.updateState({
recoveryPhraseReminderLastShown: lastShown,
});
}
/**
* Sets the last active time to the current time
* @returns {void}
@ -156,7 +180,7 @@ export default class AppStateController extends EventEmitter {
this.timer = setTimeout(
() => this.onInactiveTimeout(),
timeoutMinutes * 60 * 1000,
timeoutMinutes * MINUTE,
);
}

@ -4,9 +4,10 @@ import { warn } from 'loglevel';
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
import { MINUTE } from '../../../shared/constants/time';
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000;
const DEFAULT_INTERVAL = MINUTE * 3;
/**
* A controller that polls for token exchange

@ -18,8 +18,9 @@ import {
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time';
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
/**
* @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta

@ -19,6 +19,7 @@ import {
TRANSACTION_TYPES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction';
import { MILLISECOND } from '../../../shared/constants/time';
const IncomingTransactionsController = proxyquire('./incoming-transactions', {
'../../../shared/modules/random-id': { default: () => 54321 },
@ -26,7 +27,7 @@ const IncomingTransactionsController = proxyquire('./incoming-transactions', {
const FAKE_CHAIN_ID = '0x1338';
const MOCK_SELECTED_ADDRESS = '0x0101';
const SET_STATE_TIMEOUT = 10;
const SET_STATE_TIMEOUT = MILLISECOND * 10;
const EXISTING_INCOMING_TX = { id: 777, hash: '0x123456' };
const PREPOPULATED_INCOMING_TXS_BY_HASH = {

@ -6,9 +6,10 @@ import createInflightMiddleware 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 { PollingBlockTracker } from 'eth-block-tracker';
import { SECOND } from '../../../../shared/constants/time';
const inTest = process.env.IN_TEST === 'true';
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {};
const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {};
const getTestMiddlewares = () => {
return inTest ? [createEstimateGasDelayTestMiddleware()] : [];
};
@ -51,7 +52,7 @@ function createChainIdMiddleware(chainId) {
function createEstimateGasDelayTestMiddleware() {
return createAsyncMiddleware(async (req, _, next) => {
if (req.method === 'eth_estimateGas') {
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
}
return next();
});

@ -19,6 +19,7 @@ import {
RINKEBY_CHAIN_ID,
INFURA_BLOCKED_KEY,
} from '../../../../shared/constants/network';
import { SECOND } from '../../../../shared/constants/time';
import {
isPrefixedFormattedHexString,
isSafeChainId,
@ -29,7 +30,7 @@ import createInfuraClient from './createInfuraClient';
import createJsonRpcClient from './createJsonRpcClient';
const env = process.env.METAMASK_ENV;
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
let defaultProviderConfigOpts;
if (process.env.IN_TEST === 'true') {
@ -205,7 +206,7 @@ export default class NetworkController extends EventEmitter {
});
}
async setProviderType(type, rpcUrl = '', ticker = 'ETH', nickname = '') {
async setProviderType(type) {
assert.notStrictEqual(
type,
NETWORK_TYPE_RPC,
@ -216,7 +217,13 @@ export default class NetworkController extends EventEmitter {
`Unknown Infura provider type "${type}".`,
);
const { chainId } = NETWORK_TYPE_TO_ID_MAP[type];
this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname });
this.setProviderConfig({
type,
rpcUrl: '',
chainId,
ticker: 'ETH',
nickname: '',
});
}
resetConnection() {

@ -1,4 +1,5 @@
import { strict as assert } from 'assert';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import { txMetaStub } from '../../../../test/stub/tx-meta-stub';
import {
createPendingNonceMiddleware,
@ -55,7 +56,7 @@ describe('PendingNonceMiddleware', function () {
blockHash: null,
blockNumber: null,
from: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x1e8480',
hash:
'0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',

@ -14,6 +14,7 @@ import {
SWAPS_FETCH_ORDER_CONFLICT,
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
} from '../../../shared/constants/swaps';
import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils';
import {
@ -21,6 +22,7 @@ import {
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime,
} from '../../../ui/pages/swaps/swaps.util';
import { MINUTE, SECOND } from '../../../shared/constants/time';
import { NETWORK_EVENTS } from './network';
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
@ -32,11 +34,11 @@ const POLL_COUNT_LIMIT = 3;
// If for any reason the MetaSwap API fails to provide a refresh time,
// provide a reasonable fallback to avoid further errors
const FALLBACK_QUOTE_REFRESH_TIME = 60000;
const FALLBACK_QUOTE_REFRESH_TIME = MINUTE;
// This is the amount of time to wait, after successfully fetching quotes
// and their gas estimates, before fetching for new quotes
const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000;
const QUOTE_POLLING_DIFFERENCE_INTERVAL = SECOND * 10;
function calculateGasEstimateWithRefund(
maxGas = MAX_GAS_LIMIT,
@ -346,7 +348,7 @@ export default class SwapsController {
const gasTimeout = setTimeout(() => {
gasTimedOut = true;
resolve({ gasLimit: null, simulationFails: true });
}, 5000);
}, SECOND * 5);
// Remove gas from params that will be passed to the `estimateGas` call
// Including it can cause the estimate to fail if the actual gas needed

@ -12,6 +12,7 @@ import {
} from '../../../shared/constants/network';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../test/stub/provider';
import { SECOND } from '../../../shared/constants/time';
import SwapsController, { utils } from './swaps';
import { NETWORK_EVENTS } from './network';
@ -34,6 +35,8 @@ const TEST_AGG_ID_6 = 'TEST_AGG_6';
const TEST_AGG_ID_BEST = 'TEST_AGG_BEST';
const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL';
const POLLING_TIMEOUT = SECOND * 1000;
const MOCK_APPROVAL_NEEDED = {
data:
'0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00',
@ -836,7 +839,7 @@ describe('SwapsController', function () {
it('clears polling timeout', function () {
swapsController.pollingTimeout = setTimeout(
() => assert.fail(),
1000000,
POLLING_TIMEOUT,
);
swapsController.resetSwapsState();
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1);
@ -847,7 +850,7 @@ describe('SwapsController', function () {
it('clears polling timeout', function () {
swapsController.pollingTimeout = setTimeout(
() => assert.fail(),
1000000,
POLLING_TIMEOUT,
);
swapsController.stopPollingForQuotes();
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1);
@ -865,7 +868,7 @@ describe('SwapsController', function () {
it('clears polling timeout', function () {
swapsController.pollingTimeout = setTimeout(
() => assert.fail(),
1000000,
POLLING_TIMEOUT,
);
swapsController.resetPostFetchState();
assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1);

@ -3,11 +3,12 @@ import log from 'loglevel';
import { normalize as normalizeAddress } from 'eth-sig-util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import { MINUTE, SECOND } from '../../../shared/constants/time';
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000;
const DEFAULT_INTERVAL = MINUTE * 3;
/**
* A controller that polls for token exchange

@ -23,6 +23,7 @@ import {
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils';
import PendingTransactionTracker from './pending-tx-tracker';
@ -30,7 +31,6 @@ import * as txUtils from './lib/util';
const hstInterface = new ethers.utils.Interface(abi);
const SIMPLE_GAS_COST = '0x5208'; // Hex for 21000, cost of a simple send.
const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory
/**
@ -366,7 +366,7 @@ export default class TransactionController extends EventEmitter {
}
// This is a standard ether simple send, gas requirement is exactly 21k
return { gasLimit: SIMPLE_GAS_COST };
return { gasLimit: GAS_LIMITS.SIMPLE };
}
const {
@ -404,7 +404,7 @@ export default class TransactionController extends EventEmitter {
from,
to: from,
nonce,
gas: customGasLimit || '0x5208',
gas: customGasLimit || GAS_LIMITS.SIMPLE,
value: '0x0',
gasPrice: newGasPrice,
},

@ -13,6 +13,7 @@ import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction';
import { SECOND } from '../../../../shared/constants/time';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import TransactionController from '.';
@ -468,7 +469,7 @@ describe('Transaction Controller', function () {
},
};
// eslint-disable-next-line @babel/no-invalid-this
this.timeout(15000);
this.timeout(SECOND * 15);
const wrongValue = '0x05';
txController.addTransaction(txMeta);

@ -1,18 +1,36 @@
import { ObservableStore } from '@metamask/obs-store';
/**
* @typedef {import('@metamask/controllers').ControllerMessenger} ControllerMessenger
*/
/**
* An ObservableStore that can composes a flat
* structure of child stores based on configuration
*/
export default class ComposableObservableStore extends ObservableStore {
/**
* Describes which stores are being composed. The key is the name of the
* store, and the value is either an ObserableStore, or a controller that
* extends one of the two base controllers in the `@metamask/controllers`
* package.
* @type {Record<string, Object>}
*/
config = {};
/**
* Create a new store
*
* @param {Object} [initState] - The initial store state
* @param {Object} [config] - Map of internal state keys to child stores
* @param {Object} options
* @param {Object} [options.config] - Map of internal state keys to child stores
* @param {ControllerMessenger} options.controllerMessenger - The controller
* messenger, used for subscribing to events from BaseControllerV2-based
* controllers.
* @param {Object} [options.state] - The initial store state
*/
constructor(initState, config) {
super(initState);
constructor({ config, controllerMessenger, state }) {
super(state);
this.controllerMessenger = controllerMessenger;
if (config) {
this.updateStructure(config);
}
@ -21,15 +39,31 @@ export default class ComposableObservableStore extends ObservableStore {
/**
* Composes a new internal store subscription structure
*
* @param {Object} [config] - Map of internal state keys to child stores
* @param {Record<string, Object>} config - Describes which stores are being
* composed. The key is the name of the store, and the value is either an
* ObserableStore, or a controller that extends one of the two base
* controllers in the `@metamask/controllers` package.
*/
updateStructure(config) {
this.config = config;
this.removeAllListeners();
for (const key of Object.keys(this.config)) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
for (const key of Object.keys(config)) {
if (!config[key]) {
throw new Error(`Undefined '${key}'`);
}
const store = config[key];
if (store.subscribe) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
} else {
this.controllerMessenger.subscribe(
`${store.name}:stateChange`,
(state) => {
this.updateState({ [key]: state });
},
);
}
}
}

@ -1,40 +1,194 @@
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import {
BaseController,
BaseControllerV2,
ControllerMessenger,
} from '@metamask/controllers';
import ComposableObservableStore from './ComposableObservableStore';
class OldExampleController extends BaseController {
name = 'OldExampleController';
defaultState = {
baz: 'baz',
};
constructor() {
super();
this.initialize();
}
updateBaz(contents) {
this.update({ baz: contents });
}
}
class ExampleController extends BaseControllerV2 {
static defaultState = {
bar: 'bar',
};
static metadata = {
bar: { persist: true, anonymous: true },
};
constructor({ messenger }) {
super({
messenger,
name: 'ExampleController',
metadata: ExampleController.metadata,
state: ExampleController.defaultState,
});
}
updateBar(contents) {
this.update(() => {
return { bar: contents };
});
}
}
describe('ComposableObservableStore', function () {
it('should register initial state', function () {
const store = new ComposableObservableStore('state');
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({
controllerMessenger,
state: 'state',
});
assert.strictEqual(store.getState(), 'state');
});
it('should register initial structure', function () {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore(null, { TestStore: testStore });
const store = new ComposableObservableStore({
config: { TestStore: testStore },
controllerMessenger,
});
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
});
it('should update structure', function () {
it('should update structure with observable store', function () {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ TestStore: testStore });
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
});
it('should update structure with BaseController-based controller', function () {
const controllerMessenger = new ControllerMessenger();
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ OldExample: oldExampleController });
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), { OldExample: { baz: 'state' } });
});
it('should update structure with BaseControllerV2-based controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ Example: exampleController });
exampleController.updateBar('state');
console.log(exampleController.state);
assert.deepEqual(store.getState(), { Example: { bar: 'state' } });
});
it('should update structure with all three types of stores', function () {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({
Example: exampleController,
OldExample: oldExampleController,
Store: exampleStore,
});
exampleStore.putState('state');
exampleController.updateBar('state');
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), {
Example: { bar: 'state' },
OldExample: { baz: 'state' },
Store: 'state',
});
});
it('should return flattened state', function () {
const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' });
const barStore = new ObservableStore({ bar: 'bar' });
const store = new ComposableObservableStore(null, {
FooStore: fooStore,
BarStore: barStore,
const barController = new ExampleController({
messenger: controllerMessenger,
});
const bazController = new OldExampleController();
const store = new ComposableObservableStore({
config: {
FooStore: fooStore,
BarStore: barController,
BazStore: bazController,
},
controllerMessenger,
state: {
FooStore: fooStore.getState(),
BarStore: barController.state,
BazStore: bazController.state,
},
});
assert.deepEqual(store.getFlatState(), {
foo: 'foo',
bar: 'bar',
baz: 'baz',
});
assert.deepEqual(store.getFlatState(), { foo: 'foo', bar: 'bar' });
});
it('should return empty flattened state when not configured', function () {
const store = new ComposableObservableStore();
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({ controllerMessenger });
assert.deepEqual(store.getFlatState(), {});
});
it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: exampleController,
},
}),
);
});
it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({});
assert.throws(() => store.updateStructure({ Example: exampleController }));
});
it('should throw if initialized with undefined config entry', function () {
const controllerMessenger = new ControllerMessenger();
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: undefined,
},
controllerMessenger,
}),
);
});
});

@ -1,8 +1,9 @@
import extension from 'extensionizer';
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
import { SECOND } from '../../../../shared/constants/time';
import resolveEnsToIpfsContentId from './resolver';
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
const supportedTopLevelDomains = ['eth'];

@ -1,7 +1,8 @@
import log from 'loglevel';
import { SECOND } from '../../../shared/constants/time';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
const FIXTURE_SERVER_HOST = 'localhost';
const FIXTURE_SERVER_PORT = 12345;

@ -1,4 +1,5 @@
import Analytics from 'analytics-node';
import { SECOND } from '../../../shared/constants/time';
const isDevOrTestEnvironment = Boolean(
process.env.METAMASK_DEBUG || process.env.IN_TEST,
@ -21,7 +22,7 @@ const SEGMENT_FLUSH_AT =
// deal with short lived sessions that happen faster than the interval
// e.g confirmations. This is set to 5,000ms (5 seconds) arbitrarily with the
// intent of having a value less than 10 seconds.
const SEGMENT_FLUSH_INTERVAL = 5000;
const SEGMENT_FLUSH_INTERVAL = SECOND * 5;
/**
* Creates a mock segment module for usage in test environments. This is used

@ -20,6 +20,7 @@ import contractMap from '@metamask/contract-metadata';
import {
AddressBookController,
ApprovalController,
ControllerMessenger,
CurrencyRateController,
PhishingController,
NotificationController,
@ -28,6 +29,7 @@ import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
import { MAINNET_CHAIN_ID } from '../../shared/constants/network';
import { UI_NOTIFICATIONS } from '../../shared/notifications';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { MILLISECOND } from '../../shared/constants/time';
import ComposableObservableStore from './lib/ComposableObservableStore';
import AccountTracker from './lib/account-tracker';
@ -81,7 +83,10 @@ export default class MetamaskController extends EventEmitter {
this.defaultMaxListeners = 20;
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200);
this.sendUpdate = debounce(
this.privateSendUpdate.bind(this),
MILLISECOND * 200,
);
this.opts = opts;
this.extension = opts.extension;
this.platform = opts.platform;
@ -96,8 +101,13 @@ export default class MetamaskController extends EventEmitter {
this.getRequestAccountTabIds = opts.getRequestAccountTabIds;
this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds;
const controllerMessenger = new ControllerMessenger();
// observable state store
this.store = new ComposableObservableStore(initState);
this.store = new ComposableObservableStore({
state: initState,
controllerMessenger,
});
// external connections by origin
// Do not modify directly. Use the associated methods.
@ -157,10 +167,14 @@ export default class MetamaskController extends EventEmitter {
preferencesStore: this.preferencesController.store,
});
this.currencyRateController = new CurrencyRateController(
{ includeUSDRate: true },
initState.CurrencyController,
);
const currencyRateMessenger = controllerMessenger.getRestricted({
name: 'CurrencyRateController',
});
this.currencyRateController = new CurrencyRateController({
includeUSDRate: true,
messenger: currencyRateMessenger,
state: initState.CurrencyController,
});
this.phishingController = new PhishingController();
@ -222,10 +236,12 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.start();
this.incomingTransactionsController.start();
this.tokenRatesController.start();
this.currencyRateController.start();
} else {
this.accountTracker.stop();
this.incomingTransactionsController.stop();
this.tokenRatesController.stop();
this.currencyRateController.stop();
}
});
@ -364,18 +380,15 @@ export default class MetamaskController extends EventEmitter {
}
});
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
this.setCurrentCurrency(
this.currencyRateController.state.currentCurrency,
(error) => {
if (error) {
throw error;
}
},
);
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => {
const { ticker } = this.networkController.getProviderConfig();
try {
await this.currencyRateController.setNativeCurrency(ticker);
} catch (error) {
// TODO: Handle failure to get conversion rate more gracefully
console.error(error);
}
});
const { ticker } = this.networkController.getProviderConfig();
this.currencyRateController.configure({ nativeCurrency: ticker ?? 'ETH' });
this.networkController.lookupNetwork();
this.messageManager = new MessageManager();
this.personalMessageManager = new PersonalMessageManager();
@ -439,33 +452,37 @@ export default class MetamaskController extends EventEmitter {
NotificationController: this.notificationController,
});
this.memStore = new ComposableObservableStore(null, {
AppStateController: this.appStateController.store,
NetworkController: this.networkController.store,
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
CachedBalancesController: this.cachedBalancesController.store,
TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore,
KeyringController: this.keyringController.memStore,
PreferencesController: this.preferencesController.store,
MetaMetricsController: this.metaMetricsController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store,
IncomingTransactionsController: this.incomingTransactionsController.store,
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
ThreeBoxController: this.threeBoxController.store,
SwapsController: this.swapsController.store,
EnsController: this.ensController.store,
ApprovalController: this.approvalController,
NotificationController: this.notificationController,
this.memStore = new ComposableObservableStore({
config: {
AppStateController: this.appStateController.store,
NetworkController: this.networkController.store,
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
CachedBalancesController: this.cachedBalancesController.store,
TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore,
KeyringController: this.keyringController.memStore,
PreferencesController: this.preferencesController.store,
MetaMetricsController: this.metaMetricsController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store,
IncomingTransactionsController: this.incomingTransactionsController
.store,
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
ThreeBoxController: this.threeBoxController.store,
SwapsController: this.swapsController.store,
EnsController: this.ensController.store,
ApprovalController: this.approvalController,
NotificationController: this.notificationController,
},
controllerMessenger,
});
this.memStore.subscribe(this.sendUpdate.bind(this));
@ -649,7 +666,11 @@ export default class MetamaskController extends EventEmitter {
return {
// etc
getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
setCurrentCurrency: nodeify(
this.currencyRateController.setCurrentCurrency.bind(
this.currencyRateController,
),
),
setUseBlockie: this.setUseBlockie.bind(this),
setUseNonceField: this.setUseNonceField.bind(this),
setUsePhishDetect: this.setUsePhishDetect.bind(this),
@ -763,6 +784,14 @@ export default class MetamaskController extends EventEmitter {
this.appStateController.setConnectedStatusPopoverHasBeenShown,
this.appStateController,
),
setRecoveryPhraseReminderHasBeenShown: nodeify(
this.appStateController.setRecoveryPhraseReminderHasBeenShown,
this.appStateController,
),
setRecoveryPhraseReminderLastShown: nodeify(
this.appStateController.setRecoveryPhraseReminderLastShown,
this.appStateController,
),
// EnsController
tryReverseResolveAddress: nodeify(
@ -2511,29 +2540,6 @@ export default class MetamaskController extends EventEmitter {
// Log blocks
/**
* A method for setting the user's preferred display currency.
* @param {string} currencyCode - The code of the preferred currency.
* @param {Function} cb - A callback function returning currency info.
*/
setCurrentCurrency(currencyCode, cb) {
const { ticker } = this.networkController.getProviderConfig();
try {
const currencyState = {
nativeCurrency: ticker,
currentCurrency: currencyCode,
};
this.currencyRateController.update(currencyState);
this.currencyRateController.configure(currencyState);
cb(null);
return;
} catch (err) {
cb(err);
// eslint-disable-next-line no-useless-return
return;
}
}
/**
* A method for selecting a custom URL for an ethereum RPC provider and updating it
* @param {string} rpcUrl - A URL for a valid Ethereum RPC API.

@ -654,46 +654,24 @@ describe('MetaMaskController', function () {
});
describe('#setCustomRpc', function () {
let rpcUrl;
beforeEach(function () {
rpcUrl = metamaskController.setCustomRpc(
it('returns custom RPC that when called', async function () {
const rpcUrl = await metamaskController.setCustomRpc(
CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID,
);
assert.equal(rpcUrl, CUSTOM_RPC_URL);
});
it('returns custom RPC that when called', async function () {
assert.equal(await rpcUrl, CUSTOM_RPC_URL);
});
it('changes the network controller rpc', function () {
it('changes the network controller rpc', async function () {
await metamaskController.setCustomRpc(
CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID,
);
const networkControllerState = metamaskController.networkController.store.getState();
assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL);
});
});
describe('#setCurrentCurrency', function () {
let defaultMetaMaskCurrency;
beforeEach(function () {
defaultMetaMaskCurrency =
metamaskController.currencyRateController.state.currentCurrency;
});
it('defaults to usd', function () {
assert.equal(defaultMetaMaskCurrency, 'usd');
});
it('sets currency to JPY', function () {
metamaskController.setCurrentCurrency('JPY', noop);
assert.equal(
metamaskController.currencyRateController.state.currentCurrency,
'JPY',
);
});
});
describe('#addNewAccount', function () {
it('errors when an primary keyring is does not exist', async function () {
const addNewAccount = metamaskController.addNewAccount();

@ -0,0 +1,32 @@
import { cloneDeep } from 'lodash';
const version = 61;
/**
* Initialize attributes related to recovery seed phrase reminder
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
const currentTime = new Date().getTime();
if (state.AppStateController) {
state.AppStateController.recoveryPhraseReminderHasBeenShown = false;
state.AppStateController.recoveryPhraseReminderLastShown = currentTime;
} else {
state.AppStateController = {
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: currentTime,
};
}
return state;
}

@ -0,0 +1,67 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import migration61 from './061';
describe('migration #61', function () {
let dateStub;
beforeEach(function () {
dateStub = sinon.stub(Date.prototype, 'getTime').returns(1621580400000);
});
afterEach(function () {
dateStub.restore();
});
it('should update the version metadata', async function () {
const oldStorage = {
meta: {
version: 60,
},
data: {},
};
const newStorage = await migration61.migrate(oldStorage);
assert.deepEqual(newStorage.meta, {
version: 61,
});
});
it('should set recoveryPhraseReminderHasBeenShown to false and recoveryPhraseReminderLastShown to the current time', async function () {
const oldStorage = {
meta: {},
data: {
AppStateController: {
existingProperty: 'foo',
},
},
};
const newStorage = await migration61.migrate(oldStorage);
assert.deepEqual(newStorage.data, {
AppStateController: {
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: 1621580400000,
existingProperty: 'foo',
},
});
});
it('should initialize AppStateController if it does not exist', async function () {
const oldStorage = {
meta: {},
data: {
existingProperty: 'foo',
},
};
const newStorage = await migration61.migrate(oldStorage);
assert.deepEqual(newStorage.data, {
existingProperty: 'foo',
AppStateController: {
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: 1621580400000,
},
});
});
});

@ -65,6 +65,7 @@ const migrations = [
require('./058').default,
require('./059').default,
require('./060').default,
require('./061').default,
];
export default migrations;

@ -1,8 +1,8 @@
import extension from 'extensionizer';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { getEnvironmentType, checkForError } from '../lib/util';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import { getBlockExplorerUrlForTx } from '../../../shared/modules/transaction.utils';
export default class ExtensionPlatform {
//
@ -192,7 +192,7 @@ export default class ExtensionPlatform {
_showConfirmedTransaction(txMeta, rpcPrefs) {
this._subscribeToNotificationClicked();
const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs);
const url = getBlockExplorerLink(txMeta, rpcPrefs);
const nonce = parseInt(txMeta.txParams.nonce, 16);
const title = 'Confirmed transaction';

@ -0,0 +1,134 @@
const spawn = require('cross-spawn');
/**
* Run a command to completion using the system shell.
*
* This will run a command with the specified arguments, and resolve when the
* process has exited. The STDOUT stream is monitored for output, which is
* returned after being split into lines. All output is expected to be UTF-8
* encoded, and empty lines are removed from the output.
*
* Anything received on STDERR is assumed to indicate a problem, and is tracked
* as an error.
*
* @param {string} command - The command to run
* @param {Array<string>} [args] - The arguments to pass to the command
* @returns {Array<string>} Lines of output received via STDOUT
*/
async function runCommand(command, args) {
const output = [];
let mostRecentError;
let errorSignal;
let errorCode;
const internalError = new Error('Internal');
try {
await new Promise((resolve, reject) => {
const childProcess = spawn(command, args, { encoding: 'utf8' });
childProcess.stdout.setEncoding('utf8');
childProcess.stderr.setEncoding('utf8');
childProcess.on('error', (error) => {
mostRecentError = error;
});
childProcess.stdout.on('data', (message) => {
const nonEmptyLines = message.split('\n').filter((line) => line !== '');
output.push(...nonEmptyLines);
});
childProcess.stderr.on('data', (message) => {
mostRecentError = new Error(message.trim());
});
childProcess.once('exit', (code, signal) => {
if (code === 0) {
return resolve();
}
errorCode = code;
errorSignal = signal;
return reject(internalError);
});
});
} catch (error) {
/**
* The error is re-thrown here in an `async` context to preserve the stack trace. If this was
* was thrown inside the Promise constructor, the stack trace would show a few frames of
* Node.js internals then end, without indicating where `runCommand` was called.
*/
if (error === internalError) {
let errorMessage;
if (errorCode !== null && errorSignal !== null) {
errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`;
} else if (errorSignal !== null) {
errorMessage = `Terminaled by signal '${errorSignal}'`;
} else if (errorCode === null) {
errorMessage = 'Exited with no code or signal';
} else {
errorMessage = `Exited with code '${errorCode}'`;
}
const improvedError = new Error(errorMessage);
if (mostRecentError) {
improvedError.cause = mostRecentError;
}
throw improvedError;
}
}
return output;
}
/**
* Run a command to using the system shell.
*
* This will run a command with the specified arguments, and resolve when the
* process has exited. The STDIN, STDOUT and STDERR streams are inherited,
* letting the command take over completely until it completes. The success or
* failure of the process is determined entirely by the exit code; STDERR
* output is not used to indicate failure.
*
* @param {string} command - The command to run
* @param {Array<string>} [args] - The arguments to pass to the command
*/
async function runInShell(command, args) {
let errorSignal;
let errorCode;
const internalError = new Error('Internal');
try {
await new Promise((resolve, reject) => {
const childProcess = spawn(command, args, {
encoding: 'utf8',
stdio: 'inherit',
});
childProcess.once('exit', (code, signal) => {
if (code === 0) {
return resolve();
}
errorCode = code;
errorSignal = signal;
return reject(internalError);
});
});
} catch (error) {
/**
* The error is re-thrown here in an `async` context to preserve the stack trace. If this was
* was thrown inside the Promise constructor, the stack trace would show a few frames of
* Node.js internals then end, without indicating where `runInShell` was called.
*/
if (error === internalError) {
let errorMessage;
if (errorCode !== null && errorSignal !== null) {
errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`;
} else if (errorSignal !== null) {
errorMessage = `Terminaled by signal '${errorSignal}'`;
} else if (errorCode === null) {
errorMessage = 'Exited with no code or signal';
} else {
errorMessage = `Exited with code '${errorCode}'`;
}
const improvedError = new Error(errorMessage);
throw improvedError;
}
}
}
module.exports = { runCommand, runInShell };

@ -1,9 +1,6 @@
#!/usr/bin/env node
const childProcess = require('child_process');
const pify = require('pify');
const exec = pify(childProcess.exec, { multiArgs: true });
const VERSION = require('../dist/chrome/manifest.json').version; // eslint-disable-line import/no-unresolved
const { runCommand, runInShell } = require('./lib/run-command');
start().catch((error) => {
console.error(error);
@ -31,11 +28,17 @@ async function start() {
} else {
// create sentry release
console.log(`creating Sentry release for "${VERSION}"...`);
await exec(`sentry-cli releases new ${VERSION}`);
await runCommand('sentry-cli', ['releases', 'new', VERSION]);
console.log(
`removing any existing files from Sentry release "${VERSION}"...`,
);
await exec(`sentry-cli releases files ${VERSION} delete --all`);
await runCommand('sentry-cli', [
'releases',
'files',
VERSION,
'delete',
'--all',
]);
}
// check if version has artifacts or not
@ -49,34 +52,43 @@ async function start() {
}
// upload sentry source and sourcemaps
await exec(`./development/sentry-upload-artifacts.sh --release ${VERSION}`);
await runInShell('./development/sentry-upload-artifacts.sh', [
'--release',
VERSION,
]);
}
async function checkIfAuthWorks() {
const itWorked = await doesNotFail(async () => {
await exec(`sentry-cli releases list`);
});
return itWorked;
return await doesNotFail(() =>
runCommand('sentry-cli', ['releases', 'list']),
);
}
async function checkIfVersionExists() {
const versionAlreadyExists = await doesNotFail(async () => {
await exec(`sentry-cli releases info ${VERSION}`);
});
return versionAlreadyExists;
return await doesNotFail(() =>
runCommand('sentry-cli', ['releases', 'info', VERSION]),
);
}
async function checkIfVersionHasArtifacts() {
const artifacts = await exec(`sentry-cli releases files ${VERSION} list`);
const [artifact] = await runCommand('sentry-cli', [
'releases',
'files',
VERSION,
'list',
]);
// When there's no artifacts, we get a response from the shell like this ['', '']
return artifacts[0] && artifacts[0].length > 0;
return artifact?.length > 0;
}
async function doesNotFail(asyncFn) {
try {
await asyncFn();
return true;
} catch (err) {
return false;
} catch (error) {
if (error.message === `Exited with code '1'`) {
return false;
}
throw error;
}
}

@ -1,6 +1,5 @@
#!/usr/bin/env bash
set -x
set -e
set -u
set -o pipefail

@ -6,7 +6,7 @@ module.exports = {
coverageThreshold: {
global: {
branches: 32.75,
functions: 43.31,
functions: 42.9,
lines: 43.12,
statements: 43.67,
},

@ -1,14 +1,7 @@
{
"exclude": [
"*.log",
"builds",
"coverage",
"dist",
"docs",
"lavamoat",
"node:console",
"node_modules",
"patches",
"test-artifacts"
]
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
},
"include": ["ui/**/*.js", "app/**/*.js", "shared/**/*.js"]
}

@ -1,10 +1,10 @@
{
"name": "metamask-crx",
"version": "9.6.1",
"version": "9.7.0",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/metamask-extension"
"url": "https://github.com/MetaMask/metamask-extension.git"
},
"scripts": {
"setup": "yarn install && yarn setup:postinstall",
@ -96,8 +96,8 @@
"@fortawesome/fontawesome-free": "^5.13.0",
"@lavamoat/preinstall-always-fail": "^1.0.0",
"@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.22.0",
"@metamask/controllers": "^8.0.0",
"@metamask/contract-metadata": "^1.26.0",
"@metamask/controllers": "^9.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.5.0",
"@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0",
@ -138,7 +138,7 @@
"ethereum-ens-network-map": "^1.0.2",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "1.3.7",
"ethereumjs-util": "^7.0.9",
"ethereumjs-util": "^7.0.10",
"ethereumjs-wallet": "^0.6.4",
"ethers": "^5.0.8",
"ethjs": "^0.4.0",
@ -212,7 +212,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.5.5",
"@lavamoat/allow-scripts": "^1.0.6",
"@metamask/auto-changelog": "^1.0.0",
"@metamask/auto-changelog": "^2.1.0",
"@metamask/eslint-config": "^6.0.0",
"@metamask/eslint-config-jest": "^6.0.0",
"@metamask/eslint-config-mocha": "^6.0.0",
@ -272,6 +272,7 @@
"gulp-terser-js": "^5.2.2",
"gulp-watch": "^5.0.1",
"gulp-zip": "^4.0.0",
"history": "^5.0.0",
"jest": "^26.6.3",
"jsdom": "^11.2.0",
"koa": "^2.7.0",
@ -336,7 +337,8 @@
"gc-stats": false,
"github:assemblyscript/assemblyscript": false,
"tiny-secp256k1": false,
"@lavamoat/preinstall-always-fail": false
"@lavamoat/preinstall-always-fail": false,
"fsevents": false
}
}
}

@ -0,0 +1,11 @@
import { addHexPrefix } from 'ethereumjs-util';
const TWENTY_ONE_THOUSAND = 21000;
const ONE_HUNDRED_THOUSAND = 100000;
export const GAS_LIMITS = {
// maximum gasLimit of a simple send
SIMPLE: addHexPrefix(TWENTY_ONE_THOUSAND.toString(16)),
// a base estimate for token transfers.
BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)),
};

@ -63,6 +63,7 @@ const SWAPS_TESTNET_CHAIN_ID = '0x539';
const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/';
const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true,
@ -90,4 +91,5 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
[MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL,
};

@ -0,0 +1,5 @@
export const MILLISECOND = 1;
export const SECOND = MILLISECOND * 1000;
export const MINUTE = SECOND * 60;
export const HOUR = MINUTE * 60;
export const DAY = HOUR * 24;

@ -1,13 +1,14 @@
import { strict as assert } from 'assert';
import nock from 'nock';
import { MILLISECOND, SECOND } from '../constants/time';
import getFetchWithTimeout from './fetch-with-timeout';
describe('getFetchWithTimeout', function () {
it('fetches a url', async function () {
nock('https://api.infura.io').get('/money').reply(200, '{"hodl": false}');
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
const response = await (
await fetchWithTimeout('https://api.infura.io/money')
).json();
@ -19,10 +20,10 @@ describe('getFetchWithTimeout', function () {
it('throws when the request hits a custom timeout', async function () {
nock('https://api.infura.io')
.get('/moon')
.delay(2000)
.delay(SECOND * 2)
.reply(200, '{"moon": "2012-12-21T11:11:11Z"}');
const fetchWithTimeout = getFetchWithTimeout(123);
const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123);
try {
await fetchWithTimeout('https://api.infura.io/moon').then((r) =>
@ -37,10 +38,10 @@ describe('getFetchWithTimeout', function () {
it('should abort the request when the custom timeout is hit', async function () {
nock('https://api.infura.io')
.get('/moon')
.delay(2000)
.delay(SECOND * 2)
.reply(200, '{"moon": "2012-12-21T11:11:11Z"}');
const fetchWithTimeout = getFetchWithTimeout(123);
const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123);
try {
await fetchWithTimeout('https://api.infura.io/moon').then((r) =>

@ -4,9 +4,10 @@ import {
isValidChecksumAddress,
addHexPrefix,
toChecksumAddress,
zeroAddress,
} from 'ethereumjs-util';
export const BURN_ADDRESS = '0x0000000000000000000000000000000000000000';
export const BURN_ADDRESS = zeroAddress();
export function isBurnAddress(address) {
return address === BURN_ADDRESS;

@ -1,6 +1,7 @@
import { SECOND } from '../constants/time';
import getFetchWithTimeout from './fetch-with-timeout';
const fetchWithTimeout = getFetchWithTimeout(30000);
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
/**
* Makes a JSON RPC request to the given URL, with the given RPC method and params.

@ -1,96 +0,0 @@
import { strict as assert } from 'assert';
import {
MAINNET_CHAIN_ID,
MAINNET_NETWORK_ID,
ROPSTEN_CHAIN_ID,
ROPSTEN_NETWORK_ID,
} from '../../constants/network';
import { getBlockExplorerUrlForTx } from '../transaction.utils';
const tests = [
{
expected: 'https://etherscan.io/tx/0xabcd',
transaction: {
metamaskNetworkId: MAINNET_NETWORK_ID,
hash: '0xabcd',
},
},
{
expected: 'https://ropsten.etherscan.io/tx/0xdef0',
transaction: {
metamaskNetworkId: ROPSTEN_NETWORK_ID,
hash: '0xdef0',
},
rpcPrefs: {},
},
{
// test handling of `blockExplorerUrl` for a custom RPC
expected: 'https://block.explorer/tx/0xabcd',
transaction: {
metamaskNetworkId: '31',
hash: '0xabcd',
},
rpcPrefs: {
blockExplorerUrl: 'https://block.explorer',
},
},
{
// test handling of trailing `/` in `blockExplorerUrl` for a custom RPC
expected: 'https://another.block.explorer/tx/0xdef0',
transaction: {
networkId: '33',
hash: '0xdef0',
},
rpcPrefs: {
blockExplorerUrl: 'https://another.block.explorer/',
},
},
{
expected: 'https://etherscan.io/tx/0xabcd',
transaction: {
chainId: MAINNET_CHAIN_ID,
hash: '0xabcd',
},
},
{
expected: 'https://ropsten.etherscan.io/tx/0xdef0',
transaction: {
chainId: ROPSTEN_CHAIN_ID,
hash: '0xdef0',
},
rpcPrefs: {},
},
{
// test handling of `blockExplorerUrl` for a custom RPC
expected: 'https://block.explorer/tx/0xabcd',
transaction: {
chainId: '0x1f',
hash: '0xabcd',
},
rpcPrefs: {
blockExplorerUrl: 'https://block.explorer',
},
},
{
// test handling of trailing `/` in `blockExplorerUrl` for a custom RPC
expected: 'https://another.block.explorer/tx/0xdef0',
transaction: {
chainId: '0x21',
hash: '0xdef0',
},
rpcPrefs: {
blockExplorerUrl: 'https://another.block.explorer/',
},
},
];
describe('getBlockExplorerUrlForTx', function () {
tests.forEach((test) => {
it(`should return '${test.expected}' for transaction with hash: '${test.transaction.hash}'`, function () {
assert.strictEqual(
getBlockExplorerUrlForTx(test.transaction, test.rpcPrefs),
test.expected,
);
});
});
});

@ -1,37 +1,6 @@
import {
createExplorerLink,
createExplorerLinkForChain,
} from '@metamask/etherscan-link';
export function transactionMatchesNetwork(transaction, chainId, networkId) {
if (typeof transaction.chainId !== 'undefined') {
return transaction.chainId === chainId;
}
return transaction.metamaskNetworkId === networkId;
}
/**
* build the etherscan link for a transaction by either chainId, if available
* or metamaskNetworkId as a fallback. If rpcPrefs is provided will build the
* url for the provided blockExplorerUrl.
*
* @param {Object} transaction - a transaction object from state
* @param {string} [transaction.metamaskNetworkId] - network id tx occurred on
* @param {string} [transaction.chainId] - chain id tx occurred on
* @param {string} [transaction.hash] - hash of the transaction
* @param {Object} [rpcPrefs] - the rpc preferences for the current RPC network
* @param {string} [rpcPrefs.blockExplorerUrl] - the block explorer url for RPC
* networks
* @returns {string}
*/
export function getBlockExplorerUrlForTx(transaction, rpcPrefs = {}) {
if (rpcPrefs.blockExplorerUrl) {
return `${rpcPrefs.blockExplorerUrl.replace(/\/+$/u, '')}/tx/${
transaction.hash
}`;
}
if (transaction.chainId) {
return createExplorerLinkForChain(transaction.hash, transaction.chainId);
}
return createExplorerLink(transaction.hash, transaction.metamaskNetworkId);
}

@ -0,0 +1,151 @@
{
"data": {
"AppStateController": {
"mkrMigrationReminderTimestamp": null
},
"CachedBalancesController": {
"cachedBalances": {
"4": {}
}
},
"CurrencyController": {
"conversionDate": 1575697244.188,
"conversionRate": 149.61,
"currentCurrency": "usd",
"nativeCurrency": "ETH"
},
"IncomingTransactionsController": {
"incomingTransactions": {},
"incomingTxLastFetchedBlocksByNetwork": {
"goerli": null,
"kovan": null,
"mainnet": null,
"rinkeby": 5570536
}
},
"KeyringController": {
"vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}"
},
"NetworkController": {
"network": "1337",
"provider": {
"nickname": "Localhost 8545",
"rpcUrl": "http://localhost:8545",
"chainId": "0x539",
"ticker": "ETH",
"type": "rpc"
}
},
"NotificationController": {
"notifications": {
"1": {
"isShown": true
},
"3": {
"isShown": true
},
"5": {
"isShown": true
},
"6": {
"isShown": true
}
}
},
"OnboardingController": {
"onboardingTabs": {},
"seedPhraseBackedUp": false
},
"PermissionsMetadata": {
"domainMetadata": {
"metamask.github.io": {
"icon": null,
"name": "M E T A M A S K M E S H T E S T"
}
},
"permissionsHistory": {},
"permissionsLog": [
{
"id": 746677923,
"method": "eth_accounts",
"methodType": "restricted",
"origin": "metamask.github.io",
"request": {
"id": 746677923,
"jsonrpc": "2.0",
"method": "eth_accounts",
"origin": "metamask.github.io",
"params": []
},
"requestTime": 1575697241368,
"response": {
"id": 746677923,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1575697241370,
"success": true
}
]
},
"PreferencesController": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"0x539": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST",
"decimals": 4
}
],
"rinkeby": [],
"ropsten": []
}
},
"assetImages": {},
"completedOnboarding": true,
"currentLocale": "en",
"featureFlags": {
"showIncomingTransactions": true,
"transactionTime": false
},
"firstTimeFlowType": "create",
"forgottenPassword": false,
"frequentRpcListDetail": [],
"identities": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {
"address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"name": "Account 1"
}
},
"knownMethodData": {},
"lostIdentities": {},
"metaMetricsId": null,
"metaMetricsSendCount": 0,
"participateInMetaMetrics": false,
"preferences": {
"useNativeCurrencyAsPrimaryCurrency": true
},
"selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"suggestedTokens": {},
"tokens": [
{
"address": "0x86002be4cdd922de1ccb831582bf99284b99ac12",
"symbol": "TST",
"decimals": 4
}
],
"useBlockie": false,
"useNonceField": false,
"usePhishDetect": true
},
"config": {},
"firstTimeInfo": {
"date": 1575697234195,
"version": "7.7.0"
}
},
"meta": {
"version": 40
}
}

@ -27,6 +27,7 @@ async function withFixtures(options, testSuite) {
} = options;
const fixtureServer = new FixtureServer();
const ganacheServer = new Ganache();
let secondaryGanacheServer;
let dappServer;
let segmentServer;
let segmentStub;
@ -34,6 +35,16 @@ async function withFixtures(options, testSuite) {
let webDriver;
try {
await ganacheServer.start(ganacheOptions);
if (ganacheOptions?.concurrent) {
const { port, chainId } = ganacheOptions.concurrent;
secondaryGanacheServer = new Ganache();
await secondaryGanacheServer.start({
blockTime: 2,
_chainIdRpc: chainId,
port,
vmErrorsOnRPCResponse: false,
});
}
await fixtureServer.start();
await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures));
if (dapp) {
@ -103,6 +114,9 @@ async function withFixtures(options, testSuite) {
} finally {
await fixtureServer.stop();
await ganacheServer.quit();
if (ganacheOptions?.concurrent) {
await secondaryGanacheServer.quit();
}
if (webDriver) {
await webDriver.quit();
}

@ -1506,55 +1506,4 @@ describe('MetaMask', function () {
});
});
});
describe('Hide token', function () {
it('hides the token when clicked', async function () {
await driver.clickElement({ text: 'Assets', tag: 'button' });
await driver.clickElement({ text: 'TST', tag: 'span' });
await driver.clickElement('[data-testid="asset-options__button"]');
await driver.clickElement('[data-testid="asset-options__hide"]');
// wait for confirm hide modal to be visible
const confirmHideModal = await driver.findVisibleElement('span .modal');
await driver.clickElement(
'[data-testid="hide-token-confirmation__hide"]',
);
// wait for confirm hide modal to be removed from DOM.
await confirmHideModal.waitForElementState('hidden');
});
});
describe('Add existing token using search', function () {
it('clicks on the Add Token button', async function () {
await driver.clickElement({ text: 'Add Token', tag: 'button' });
await driver.delay(regularDelayMs);
});
it('can pick a token from the existing options', async function () {
await driver.fill('#search-tokens', 'BAT');
await driver.delay(regularDelayMs);
await driver.clickElement({ text: 'BAT', tag: 'span' });
await driver.delay(regularDelayMs);
await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.delay(regularDelayMs);
await driver.clickElement({ text: 'Add Tokens', tag: 'button' });
await driver.delay(largeDelayMs);
});
it('renders the balance for the chosen token', async function () {
await driver.waitForSelector({
css: '.token-overview__primary-balance',
text: '0 BAT',
});
await driver.delay(regularDelayMs);
});
});
});

@ -0,0 +1,94 @@
const { strict: assert } = require('assert');
const { withFixtures } = require('../helpers');
describe('Hide token', function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
it('hides the token when clicked', async function () {
await withFixtures(
{
fixtures: 'custom-token',
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.waitForSelector({
css: '.asset-list-item__token-button',
text: '0 TST',
});
let assets = await driver.findElements('.asset-list-item');
assert.equal(assets.length, 2);
await driver.clickElement({ text: 'Assets', tag: 'button' });
await driver.clickElement({ text: 'TST', tag: 'span' });
await driver.clickElement('[data-testid="asset-options__button"]');
await driver.clickElement('[data-testid="asset-options__hide"]');
// wait for confirm hide modal to be visible
const confirmHideModal = await driver.findVisibleElement('span .modal');
await driver.clickElement(
'[data-testid="hide-token-confirmation__hide"]',
);
// wait for confirm hide modal to be removed from DOM.
await confirmHideModal.waitForElementState('hidden');
assets = await driver.findElements('.asset-list-item');
assert.equal(assets.length, 1);
},
);
});
});
describe('Add existing token using search', function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
it('renders the balance for the chosen token', 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({ text: 'Add Token', tag: 'button' });
await driver.fill('#search-tokens', 'BAT');
await driver.clickElement({ text: 'BAT', tag: 'span' });
await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.clickElement({ text: 'Add Tokens', tag: 'button' });
await driver.waitForSelector({
css: '.token-overview__primary-balance',
text: '0 BAT',
});
},
);
});
});

@ -12,10 +12,12 @@ describe('Stores custom RPC history', function () {
],
};
it(`creates first custom RPC entry`, async function () {
const port = 8546;
const chainId = 1338;
await withFixtures(
{
fixtures: 'imported-account',
ganacheOptions,
ganacheOptions: { ...ganacheOptions, concurrent: { port, chainId } },
title: this.test.title,
},
async ({ driver }) => {
@ -23,8 +25,8 @@ describe('Stores custom RPC history', function () {
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
const rpcUrl = 'http://127.0.0.1:8545/1';
const chainId = '0x539'; // Ganache default, decimal 1337
const rpcUrl = `http://127.0.0.1:${port}`;
const networkName = 'Secondary Ganache Testnet';
await driver.clickElement('.network-display');
@ -33,17 +35,95 @@ describe('Stores custom RPC history', function () {
await driver.findElement('.settings-page__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
const networkNameInput = customRpcInputs[0];
const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
await networkNameInput.clear();
await networkNameInput.sendKeys(networkName);
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(rpcUrl);
await chainIdInput.clear();
await chainIdInput.sendKeys(chainId);
await chainIdInput.sendKeys(chainId.toString());
await driver.clickElement('.network-form__footer .btn-secondary');
await driver.findElement({ text: rpcUrl, tag: 'div' });
await driver.findElement({ text: networkName, tag: 'div' });
},
);
});
it('warns user when they enter url for an already configured network', 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);
// duplicate network
const duplicateRpcUrl = 'http://localhost:8545';
await driver.clickElement('.network-display');
await driver.clickElement({ text: 'Custom RPC', tag: 'span' });
await driver.findElement('.settings-page__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
const rpcUrlInput = customRpcInputs[1];
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(duplicateRpcUrl);
await driver.findElement({
text: 'This URL is currently used by the Localhost 8545 network.',
tag: 'p',
});
},
);
});
it('warns user when they enter chainId for an already configured network', 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);
// duplicate network
const newRpcUrl = 'http://localhost:8544';
const duplicateChainId = '0x539';
await driver.clickElement('.network-display');
await driver.clickElement({ text: 'Custom RPC', tag: 'span' });
await driver.findElement('.settings-page__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]');
const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(newRpcUrl);
await chainIdInput.clear();
await chainIdInput.sendKeys(duplicateChainId);
await driver.findElement({
text:
'This Chain ID is currently used by the Localhost 8545 network.',
tag: 'p',
});
},
);
});

@ -1,3 +1,4 @@
import { GAS_LIMITS } from '../../shared/constants/gas';
import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
@ -16,7 +17,7 @@ export const txMetaStub = {
type: TRANSACTION_TYPES.SENT_ETHER,
txParams: {
from: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x1e8480',
to: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
value: '0x0',
@ -197,7 +198,7 @@ export const txMetaStub = {
type: TRANSACTION_TYPES.SENT_ETHER,
txParams: {
from: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x1e8480',
nonce: '0x4',
to: '0xf231d46dd78806e1dd93442cf33c7671f8538748',

@ -23,6 +23,7 @@
@import 'permission-page-container/index';
@import 'permissions-connect-footer/index';
@import 'permissions-connect-header/index';
@import 'recovery-phrase-reminder/index';
@import 'selected-account/index';
@import 'sidebars/index';
@import 'signature-request/index';

@ -10,7 +10,7 @@ import InfoIcon from '../../ui/icon/info-icon.component';
import Button from '../../ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent';
import { updateSendToken } from '../../../store/actions';
import { updateSendToken } from '../../../ducks/send/send.duck';
import { SEND_ROUTE } from '../../../helpers/constants/routes';
import { SEVERITIES } from '../../../helpers/constants/design-system';

@ -11,10 +11,10 @@ import { useMetricEvent } from '../../../hooks/useMetricEvent';
import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency';
import {
getCurrentAccountWithSendEtherInfo,
getNativeCurrency,
getShouldShowFiat,
getNativeCurrencyImage,
} from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
const AssetList = ({ onClickAsset }) => {

@ -6,7 +6,7 @@ const ConfirmPageContainerWarning = (props) => {
<div className="confirm-page-container-warning">
<img
className="confirm-page-container-warning__icon"
src="/images/alert.svg"
src="./images/alert.svg"
alt=""
/>
<div className="confirm-page-container-warning__warning">

@ -47,7 +47,7 @@ export default function ConfirmPageContainerHeader({
visibility: showEdit ? 'initial' : 'hidden',
}}
>
<img src="/images/caret-left.svg" alt="" />
<img src="./images/caret-left.svg" alt="" />
<span
className="confirm-page-container-header__back-button"
onClick={() => onEdit()}

@ -8,6 +8,11 @@ import ConfirmPageContainerHeader from './confirm-page-container-header.componen
const util = require('../../../../../app/scripts/lib/util');
jest.mock('react', () => ({
...jest.requireActual('react'),
useLayoutEffect: jest.requireActual('react').useEffect,
}));
describe('Confirm Detail Row Component', () => {
describe('render', () => {
it('should render a div with a confirm-page-container-header class', () => {

@ -33,14 +33,14 @@ const ConfirmPageContainerNavigation = (props) => {
data-testid="first-page"
onClick={() => onNextTx(firstTx)}
>
<img src="/images/double-arrow.svg" alt="" />
<img src="./images/double-arrow.svg" alt="" />
</div>
<div
className="confirm-page-container-navigation__arrow"
data-testid="previous-page"
onClick={() => onNextTx(prevTxId)}
>
<img src="/images/single-arrow.svg" alt="" />
<img src="./images/single-arrow.svg" alt="" />
</div>
</div>
<div className="confirm-page-container-navigation__textcontainer">
@ -64,7 +64,7 @@ const ConfirmPageContainerNavigation = (props) => {
>
<img
className="confirm-page-container-navigation__imageflip"
src="/images/single-arrow.svg"
src="./images/single-arrow.svg"
alt=""
/>
</div>
@ -75,7 +75,7 @@ const ConfirmPageContainerNavigation = (props) => {
>
<img
className="confirm-page-container-navigation__imageflip"
src="/images/double-arrow.svg"
src="./images/double-arrow.svg"
alt=""
/>
</div>

@ -23,6 +23,7 @@ describe('AdvancedTabContent Component', () => {
insufficientBalance={false}
customPriceIsSafe
isSpeedUp={false}
customPriceIsExcessive={false}
/>,
);
});

@ -76,6 +76,7 @@ describe('GasModalPageContainer Component', () => {
customGasLimitInHex="mockCustomGasLimitInHex"
insufficientBalance={false}
disableSave={false}
customPriceIsExcessive={false}
/>,
);
});
@ -124,6 +125,7 @@ describe('GasModalPageContainer Component', () => {
<GasModalPageContainer
fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
customPriceIsExcessive={false}
/>,
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } },
);
@ -202,6 +204,7 @@ describe('GasModalPageContainer Component', () => {
customGasLimitInHex="mockCustomGasLimitInHex"
insufficientBalance={false}
disableSave={false}
customPriceIsExcessive={false}
hideBasic
/>,
);

@ -1,6 +1,6 @@
import sinon from 'sinon';
import { hideModal, setGasLimit, setGasPrice } from '../../../../store/actions';
import { hideModal } from '../../../../store/actions';
import {
setCustomGasPrice,
@ -8,7 +8,11 @@ import {
resetCustomData,
} from '../../../../ducks/gas/gas.duck';
import { hideGasButtonGroup } from '../../../../ducks/send/send.duck';
import {
hideGasButtonGroup,
setGasLimit,
setGasPrice,
} from '../../../../ducks/send/send.duck';
let mapDispatchToProps;
let mergeProps;
@ -29,7 +33,7 @@ jest.mock('../../../../selectors', () => ({
getDefaultActiveButtonIndex: (a, b) => a + b,
getCurrentEthBalance: (state) => state.metamask.balance || '0x0',
getSendToken: () => null,
getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0',
getTokenBalance: (state) => state.send.tokenBalance || '0x0',
getCustomGasPrice: (state) => state.gas.customData.price || '0x0',
getCustomGasLimit: (state) => state.gas.customData.limit || '0x0',
getCurrentCurrency: jest.fn().mockReturnValue('usd'),
@ -44,8 +48,6 @@ jest.mock('../../../../selectors', () => ({
jest.mock('../../../../store/actions', () => ({
hideModal: jest.fn(),
setGasLimit: jest.fn(),
setGasPrice: jest.fn(),
updateTransaction: jest.fn(),
}));
@ -57,6 +59,8 @@ jest.mock('../../../../ducks/gas/gas.duck', () => ({
jest.mock('../../../../ducks/send/send.duck', () => ({
hideGasButtonGroup: jest.fn(),
setGasLimit: jest.fn(),
setGasPrice: jest.fn(),
}));
require('./gas-modal-page-container.container');

@ -2,13 +2,9 @@ import { connect } from 'react-redux';
import { addHexPrefix } from '../../../../../app/scripts/lib/util';
import {
hideModal,
setGasLimit,
setGasPrice,
createRetryTransaction,
createSpeedUpTransaction,
hideSidebar,
updateSendAmount,
setGasTotal,
updateTransaction,
} from '../../../../store/actions';
import {
@ -19,6 +15,10 @@ import {
} from '../../../../ducks/gas/gas.duck';
import {
hideGasButtonGroup,
setGasLimit,
setGasPrice,
setGasTotal,
updateSendAmount,
updateSendErrors,
} from '../../../../ducks/send/send.duck';
import {
@ -28,6 +28,7 @@ import {
getIsMainnet,
getSendToken,
getPreferences,
getIsTestnet,
getBasicGasEstimateLoadingStatus,
getCustomGasLimit,
getCustomGasPrice,
@ -36,8 +37,10 @@ import {
isCustomPriceSafe,
getTokenBalance,
getSendMaxModeState,
isCustomPriceSafeForCustomNetwork,
getAveragePriceEstimateInHexWEI,
isCustomPriceExcessive,
getIsGasEstimatesFetched,
} from '../../../../selectors';
import {
@ -55,10 +58,14 @@ import {
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants';
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils';
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction';
import { GAS_LIMITS } from '../../../../../shared/constants/gas';
import GasModalPageContainer from './gas-modal-page-container.component';
const mapStateToProps = (state, ownProps) => {
const { currentNetworkTxList, send } = state.metamask;
const {
metamask: { currentNetworkTxList },
send,
} = state;
const { modalState: { props: modalProps } = {} } = state.appState.modal || {};
const { txData = {} } = modalProps || {};
const { transaction = {}, onSubmit } = ownProps;
@ -72,7 +79,7 @@ const mapStateToProps = (state, ownProps) => {
const txParams = selectedTransaction?.txParams
? selectedTransaction.txParams
: {
gas: send.gasLimit || '0x5208',
gas: send.gasLimit || GAS_LIMITS.SIMPLE,
gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state, true),
value: sendToken ? '0x0' : send.amount,
};
@ -81,7 +88,7 @@ const mapStateToProps = (state, ownProps) => {
const value = ownProps.transaction?.txParams?.value || txParams.value;
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice;
const customModalGasLimitInHex =
getCustomGasLimit(state) || currentGasLimit || '0x5208';
getCustomGasLimit(state) || currentGasLimit || GAS_LIMITS.SIMPLE;
const customGasTotal = calcGasTotal(
customModalGasLimitInHex,
customModalGasPriceInHex,
@ -113,6 +120,7 @@ const mapStateToProps = (state, ownProps) => {
const showFiat = Boolean(isMainnet || showFiatInTestnets);
const isSendTokenSet = Boolean(sendToken);
const isTestnet = getIsTestnet(state);
const newTotalEth =
maxModeOn && !isSendTokenSet
@ -132,6 +140,16 @@ const mapStateToProps = (state, ownProps) => {
balance,
conversionRate,
});
const isGasEstimate = getIsGasEstimatesFetched(state);
let customPriceIsSafe;
if ((isMainnet || process.env.IN_TEST) && isGasEstimate) {
customPriceIsSafe = isCustomPriceSafe(state);
} else if (isTestnet) {
customPriceIsSafe = true;
} else {
customPriceIsSafe = isCustomPriceSafeForCustomNetwork(state);
}
return {
hideBasic,
@ -142,7 +160,7 @@ const mapStateToProps = (state, ownProps) => {
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
customGasTotal,
newTotalFiat,
customPriceIsSafe: isCustomPriceSafe(state),
customPriceIsSafe,
customPriceIsExcessive: isCustomPriceExcessive(state),
maxModeOn,
gasPriceButtonGroupProps: {

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Button from '../../ui/button';
import LoadingScreen from '../../ui/loading-screen';
import { SECOND } from '../../../../shared/constants/time';
export default class LoadingNetworkScreen extends PureComponent {
state = {
@ -27,7 +28,7 @@ export default class LoadingNetworkScreen extends PureComponent {
componentDidMount = () => {
this.cancelCallTimeout = setTimeout(
this.cancelCall,
this.props.cancelTime || 15000,
this.props.cancelTime || SECOND * 15,
);
};
@ -87,7 +88,7 @@ export default class LoadingNetworkScreen extends PureComponent {
window.clearTimeout(this.cancelCallTimeout);
this.cancelCallTimeout = setTimeout(
this.cancelCall,
this.props.cancelTime || 15000,
this.props.cancelTime || SECOND * 15,
);
}}
>
@ -114,7 +115,7 @@ export default class LoadingNetworkScreen extends PureComponent {
this.setState({ showErrorScreen: false });
this.cancelCallTimeout = setTimeout(
this.cancelCall,
this.props.cancelTime || 15000,
this.props.cancelTime || SECOND * 15,
);
}
};

@ -2,11 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { getAccountLink } from '@metamask/etherscan-link';
import { showModal } from '../../../store/actions';
import { CONNECTED_ROUTE } from '../../../helpers/constants/routes';
import { Menu, MenuItem } from '../../ui/menu';
import getAccountLink from '../../../helpers/utils/account-link';
import {
getCurrentChainId,
getCurrentKeyring,
@ -14,7 +14,10 @@ import {
getSelectedIdentity,
} from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent';
import {
useMetricEvent,
useNewMetricEvent,
} from '../../../hooks/useMetricEvent';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
@ -22,6 +25,14 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
const t = useI18nContext();
const dispatch = useDispatch();
const history = useHistory();
const keyring = useSelector(getCurrentKeyring);
const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const selectedIdentity = useSelector(getSelectedIdentity);
const { address } = selectedIdentity;
const addressLink = getAccountLink(address, chainId, rpcPrefs);
const openFullscreenEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
@ -36,13 +47,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
name: 'Viewed Account Details',
},
});
const viewOnEtherscanEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
});
const openConnectedSitesEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
@ -51,12 +56,16 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
},
});
const keyring = useSelector(getCurrentKeyring);
const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const selectedIdentity = useSelector(getSelectedIdentity);
const blockExplorerLinkClickedEvent = useNewMetricEvent({
category: 'Navigation',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Account Options',
block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '',
},
});
const { address } = selectedIdentity;
const isRemovable = keyring.type !== 'HD Key Tree';
return (
@ -90,9 +99,9 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
</MenuItem>
<MenuItem
onClick={() => {
viewOnEtherscanEvent();
blockExplorerLinkClickedEvent();
global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs),
url: addressLink,
});
onClose();
}}

@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { waitFor } from '@testing-library/react';
import { mountWithRouter } from '../../../../test/lib/render-helpers';
import { ROPSTEN_CHAIN_ID } from '../../../../shared/constants/network';
import MenuBar from './menu-bar';
@ -30,21 +31,25 @@ const initState = {
const mockStore = configureStore();
describe('MenuBar', () => {
it('opens account detail menu when account options is clicked', () => {
it('opens account detail menu when account options is clicked', async () => {
const store = mockStore(initState);
const wrapper = mountWithRouter(
<Provider store={store}>
<MenuBar />
</Provider>,
);
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
await waitFor(() =>
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true),
);
const accountOptions = wrapper.find('.menu-bar__account-options');
accountOptions.simulate('click');
wrapper.update();
expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
await waitFor(() =>
expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true),
);
});
it('sets accountDetailsMenuOpen to false when closed', () => {
it('sets accountDetailsMenuOpen to false when closed', async () => {
const store = mockStore(initState);
const wrapper = mountWithRouter(
<Provider store={store}>
@ -54,10 +59,14 @@ describe('MenuBar', () => {
const accountOptions = wrapper.find('.menu-bar__account-options');
accountOptions.simulate('click');
wrapper.update();
expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
await waitFor(() =>
expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true),
);
const accountDetailsMenu = wrapper.find('AccountOptionsMenu');
accountDetailsMenu.prop('onClose')();
wrapper.update();
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
await waitFor(() => {
accountDetailsMenu.prop('onClose')();
wrapper.update();
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
});
});
});

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import AccountModalContainer from '../account-modal-container';
import getAccountLink from '../../../../helpers/utils/account-link';
import QrView from '../../../ui/qr-code';
import EditableLabel from '../../../ui/editable-label';
import Button from '../../../ui/button';
@ -18,6 +19,7 @@ export default class AccountDetailsModal extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
render() {
@ -61,8 +63,20 @@ export default class AccountDetailsModal extends Component {
type="secondary"
className="account-details-modal__button"
onClick={() => {
const accountLink = getAccountLink(address, chainId, rpcPrefs);
this.context.trackEvent({
category: 'Navigation',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Account Details Modal',
block_explorer_domain: accountLink
? new URL(accountLink)?.hostname
: '',
},
});
global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs),
url: accountLink,
});
}}
>

@ -36,6 +36,7 @@ describe('Account Details Modal', () => {
wrapper = shallow(<AccountDetailsModal.WrappedComponent {...props} />, {
context: {
t: (str) => str,
trackEvent: (e) => e,
},
});
});

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import Modal from '../../modal';
import { addressSummary } from '../../../../helpers/utils/util';
import Identicon from '../../../ui/identicon';
import getAccountLink from '../../../../helpers/utils/account-link';
export default class ConfirmRemoveAccount extends Component {
static propTypes = {
@ -16,6 +16,7 @@ export default class ConfirmRemoveAccount extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
handleRemove = () => {
@ -30,7 +31,7 @@ export default class ConfirmRemoveAccount extends Component {
renderSelectedAccount() {
const { t } = this.context;
const { identity } = this.props;
const { identity, rpcPrefs, chainId } = this.props;
return (
<div className="confirm-remove-account__account">
<div className="confirm-remove-account__account__identicon">
@ -53,11 +54,27 @@ export default class ConfirmRemoveAccount extends Component {
<div className="confirm-remove-account__account__link">
<a
className=""
href={getAccountLink(
identity.address,
this.props.chainId,
this.props.rpcPrefs,
)}
onClick={() => {
const accountLink = getAccountLink(
identity.address,
chainId,
rpcPrefs,
);
this.context.trackEvent({
category: 'Accounts',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Remove Account',
block_explorer_domain: accountLink
? new URL(accountLink)?.hostname
: '',
},
});
global.platform.openTab({
url: accountLink,
});
}}
target="_blank"
rel="noopener noreferrer"
title={t('etherscanView')}

@ -21,6 +21,8 @@ describe('Confirm Remove Account', () => {
address: '0x0',
name: 'Account 1',
},
chainId: '0x0',
rpcPrefs: {},
};
const mockStore = configureStore();

@ -4,6 +4,7 @@ import log from 'loglevel';
import { BrowserQRCodeReader } from '@zxing/library';
import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { SECOND } from '../../../../../shared/constants/time';
import Spinner from '../../../ui/spinner';
import WebcamUtils from '../../../../helpers/utils/webcam-utils';
import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component';
@ -86,14 +87,14 @@ export default class QrScanner extends Component {
const { permissions } = await WebcamUtils.checkStatus();
if (permissions) {
// Let the video stream load first...
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
if (!this.mounted) {
return;
}
this.setState({ ready: READY_STATE.READY });
} else if (this.mounted) {
// Keep checking for permissions
this.permissionChecker = setTimeout(this.checkPermissions, 1000);
this.permissionChecker = setTimeout(this.checkPermissions, SECOND);
}
} catch (error) {
if (this.mounted) {

@ -0,0 +1 @@
export { default } from './recovery-phrase-reminder';

@ -0,0 +1,10 @@
.recovery-phrase-reminder {
&__list {
list-style: disc;
padding-left: 20px;
li {
margin-bottom: 5px;
}
}
}

@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { useI18nContext } from '../../../hooks/useI18nContext';
// Components
import Box from '../../ui/box';
import Button from '../../ui/button';
import Popover from '../../ui/popover';
import Typography from '../../ui/typography';
// Helpers
import {
COLORS,
DISPLAY,
TEXT_ALIGN,
TYPOGRAPHY,
BLOCK_SIZES,
FONT_WEIGHT,
JUSTIFY_CONTENT,
} from '../../../helpers/constants/design-system';
import { INITIALIZE_BACKUP_SEED_PHRASE_ROUTE } from '../../../helpers/constants/routes';
export default function RecoveryPhraseReminder({ onConfirm, hasBackedUp }) {
const t = useI18nContext();
const history = useHistory();
const handleBackUp = () => {
history.push(INITIALIZE_BACKUP_SEED_PHRASE_ROUTE);
};
return (
<Popover centerTitle title={t('recoveryPhraseReminderTitle')}>
<Box padding={[0, 4, 6, 4]} className="recovery-phrase-reminder">
<Typography
color={COLORS.BLACK}
align={TEXT_ALIGN.CENTER}
variant={TYPOGRAPHY.Paragraph}
boxProps={{ marginTop: 0, marginBottom: 4 }}
>
{t('recoveryPhraseReminderSubText')}
</Typography>
<Box margin={[4, 0, 8, 0]}>
<ul className="recovery-phrase-reminder__list">
<li>
<Typography
tag="span"
color={COLORS.BLACK}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('recoveryPhraseReminderItemOne')}
</Typography>
</li>
<li>{t('recoveryPhraseReminderItemTwo')}</li>
<li>
{hasBackedUp ? (
t('recoveryPhraseReminderHasBackedUp')
) : (
<>
{t('recoveryPhraseReminderHasNotBackedUp')}
<Box display={DISPLAY.INLINE_BLOCK} marginLeft={1}>
<Button
type="link"
onClick={handleBackUp}
style={{
fontSize: 'inherit',
padding: 0,
}}
>
{t('recoveryPhraseReminderBackupStart')}
</Button>
</Box>
</>
)}
</li>
</ul>
</Box>
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
<Box width={BLOCK_SIZES.TWO_FIFTHS}>
<Button rounded type="primary" onClick={onConfirm}>
{t('recoveryPhraseReminderConfirm')}
</Button>
</Box>
</Box>
</Box>
</Popover>
);
}
RecoveryPhraseReminder.propTypes = {
hasBackedUp: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
};

@ -5,6 +5,7 @@ import { shortenAddress } from '../../../helpers/utils/util';
import Tooltip from '../../ui/tooltip';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import { SECOND } from '../../../../shared/constants/time';
class SelectedAccount extends Component {
state = {
@ -50,7 +51,7 @@ class SelectedAccount extends Component {
this.setState({ copied: true });
this.copyTimeout = setTimeout(
() => this.setState({ copied: false }),
3000,
SECOND * 3,
);
copyToClipboard(checksummedAddress);
}}

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import CustomizeGas from '../gas-customization/gas-modal-page-container';
import { MILLISECOND } from '../../../../shared/constants/time';
export default class Sidebar extends Component {
static propTypes = {
@ -60,8 +61,8 @@ export default class Sidebar extends Component {
<div>
<ReactCSSTransitionGroup
transitionName={transitionName}
transitionEnterTimeout={300}
transitionLeaveTimeout={200}
transitionEnterTimeout={MILLISECOND * 300}
transitionLeaveTimeout={MILLISECOND * 200}
>
{sidebarOpen && !sidebarShouldClose
? this.renderSidebarContent()

@ -2,18 +2,19 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import {
getEthConversionFromWeiHex,
getValueFromWeiHex,
} from '../../../helpers/utils/conversions.util';
import { formatDate } from '../../../helpers/utils/util';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import TransactionActivityLogIcon from './transaction-activity-log-icon';
import { CONFIRMED_STATUS } from './transaction-activity-log.constants';
export default class TransactionActivityLog extends PureComponent {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
static propTypes = {
@ -31,10 +32,21 @@ export default class TransactionActivityLog extends PureComponent {
};
handleActivityClick = (activity) => {
const etherscanUrl = getBlockExplorerUrlForTx(
activity,
this.props.rpcPrefs,
);
const { rpcPrefs } = this.props;
const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs);
this.context.trackEvent({
category: 'Transactions',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Transaction Block Explorer',
action: 'Activity Details',
block_explorer_domain: etherscanUrl
? new URL(etherscanUrl)?.hostname
: '',
},
});
global.platform.openTab({ url: etherscanUrl });
};

@ -2,9 +2,9 @@ import { connect } from 'react-redux';
import { findLastIndex } from 'lodash';
import {
conversionRateSelector,
getNativeCurrency,
getRpcPrefsForCurrentProvider,
} from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import TransactionActivityLog from './transaction-activity-log.component';
import { combineTransactionHistories } from './transaction-activity-log.util';
import {

@ -1,3 +1,4 @@
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import {
ROPSTEN_CHAIN_ID,
ROPSTEN_NETWORK_ID,
@ -34,7 +35,7 @@ describe('TransactionActivityLog utils', () => {
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
value: '0x2386f26fc10000',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
},
type: TRANSACTION_TYPES.STANDARD,
@ -82,7 +83,7 @@ describe('TransactionActivityLog utils', () => {
time: 1543958845581,
txParams: {
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0x32',
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
@ -105,7 +106,7 @@ describe('TransactionActivityLog utils', () => {
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
value: '0x2386f26fc10000',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0x32',
},
@ -176,7 +177,7 @@ describe('TransactionActivityLog utils', () => {
time: 1543958857697,
txParams: {
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x481f2280',
nonce: '0x32',
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
@ -244,7 +245,7 @@ describe('TransactionActivityLog utils', () => {
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
@ -267,7 +268,7 @@ describe('TransactionActivityLog utils', () => {
time: 1535507561452,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
@ -395,7 +396,7 @@ describe('TransactionActivityLog utils', () => {
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',

@ -1,6 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import TransactionBreakdown from './transaction-breakdown.component';
describe('TransactionBreakdown Component', () => {
@ -11,7 +12,7 @@ describe('TransactionBreakdown Component', () => {
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',

@ -1,9 +1,6 @@
import { connect } from 'react-redux';
import {
getIsMainnet,
getNativeCurrency,
getPreferences,
} from '../../../selectors';
import { getIsMainnet, getPreferences } from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util';
import { sumHexes } from '../../../helpers/utils/transactions.util';
import TransactionBreakdown from './transaction-breakdown.component';

@ -56,14 +56,6 @@ export default function TransactionIcon({ status, category }) {
TransactionIcon.propTypes = {
status: PropTypes.oneOf([
TRANSACTION_GROUP_CATEGORIES.APPROVAL,
TRANSACTION_GROUP_CATEGORIES.INTERACTION,
TRANSACTION_GROUP_CATEGORIES.SEND,
TRANSACTION_GROUP_CATEGORIES.SIGNATURE_REQUEST,
TRANSACTION_GROUP_CATEGORIES.RECEIVE,
TRANSACTION_GROUP_CATEGORIES.SWAP,
]).isRequired,
category: PropTypes.oneOf([
TRANSACTION_GROUP_STATUSES.PENDING,
TRANSACTION_STATUSES.UNAPPROVED,
TRANSACTION_STATUSES.APPROVED,
@ -72,4 +64,12 @@ TransactionIcon.propTypes = {
TRANSACTION_GROUP_STATUSES.CANCELLED,
TRANSACTION_STATUSES.DROPPED,
]).isRequired,
category: PropTypes.oneOf([
TRANSACTION_GROUP_CATEGORIES.APPROVAL,
TRANSACTION_GROUP_CATEGORIES.INTERACTION,
TRANSACTION_GROUP_CATEGORIES.SEND,
TRANSACTION_GROUP_CATEGORIES.SIGNATURE_REQUEST,
TRANSACTION_GROUP_CATEGORIES.RECEIVE,
TRANSACTION_GROUP_CATEGORIES.SWAP,
]).isRequired,
};

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import copyToClipboard from 'copy-to-clipboard';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import SenderToRecipient from '../../ui/sender-to-recipient';
import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants';
import TransactionActivityLog from '../transaction-activity-log';
@ -9,13 +10,14 @@ import Button from '../../ui/button';
import Tooltip from '../../ui/tooltip';
import Copy from '../../ui/icon/copy-icon.component';
import Popover from '../../ui/popover';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import { SECOND } from '../../../../shared/constants/time';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
export default class TransactionListItemDetails extends PureComponent {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
trackEvent: PropTypes.func,
};
static defaultProps = {
@ -47,22 +49,30 @@ export default class TransactionListItemDetails extends PureComponent {
justCopied: false,
};
handleEtherscanClick = () => {
handleBlockExplorerClick = () => {
const {
transactionGroup: { primaryTransaction },
rpcPrefs,
} = this.props;
const blockExplorerLink = getBlockExplorerLink(
primaryTransaction,
rpcPrefs,
);
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Clicked "View on Etherscan"',
this.context.trackEvent({
category: 'Transactions',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Transaction Block Explorer',
action: 'Transaction Details',
block_explorer_domain: blockExplorerLink
? new URL(blockExplorerLink)?.hostname
: '',
},
});
global.platform.openTab({
url: getBlockExplorerUrlForTx(primaryTransaction, rpcPrefs),
url: blockExplorerLink,
});
};
@ -93,7 +103,7 @@ export default class TransactionListItemDetails extends PureComponent {
this.setState({ justCopied: true }, () => {
copyToClipboard(hash);
setTimeout(() => this.setState({ justCopied: false }), 1000);
setTimeout(() => this.setState({ justCopied: false }), SECOND);
});
};
@ -203,10 +213,10 @@ export default class TransactionListItemDetails extends PureComponent {
>
<Button
type="raised"
onClick={this.handleEtherscanClick}
onClick={this.handleBlockExplorerClick}
disabled={!hash}
>
<img src="/images/arrow-popout.svg" alt="" />
<img src="./images/arrow-popout.svg" alt="" />
</Button>
</Tooltip>
{showRetry && (

@ -5,6 +5,7 @@ import SenderToRecipient from '../../ui/sender-to-recipient';
import TransactionBreakdown from '../transaction-breakdown';
import TransactionActivityLog from '../transaction-activity-log';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import TransactionListItemDetails from './transaction-list-item-details.component';
describe('TransactionListItemDetails Component', () => {
@ -15,7 +16,7 @@ describe('TransactionListItemDetails Component', () => {
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
@ -57,7 +58,7 @@ describe('TransactionListItemDetails Component', () => {
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
@ -102,7 +103,7 @@ describe('TransactionListItemDetails Component', () => {
status: 'confirmed',
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',
@ -146,7 +147,7 @@ describe('TransactionListItemDetails Component', () => {
hash: '0xaa',
txParams: {
from: '0x1',
gas: '0x5208',
gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
nonce: '0xa4',
to: '0x2',

@ -22,7 +22,9 @@ export default function UserPreferencedCurrencyDisplay({
const prefixComponent = useMemo(() => {
return (
currency === ETH &&
showEthLogo && <img src="/images/eth.svg" height={ethLogoHeight} alt="" />
showEthLogo && (
<img src="./images/eth.svg" height={ethLogoHeight} alt="" />
)
);
}, [currency, showEthLogo, ethLogoHeight]);

@ -17,7 +17,7 @@ import {
} from '../../../hooks/useMetricEvent';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { updateSendToken } from '../../../store/actions';
import { updateSendToken } from '../../../ducks/send/send.duck';
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import {
getAssetImages,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save