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. 22
      .storybook/preview.js
  7. 955
      .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. 46
      app/scripts/lib/ComposableObservableStore.js
  37. 172
      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. 90
      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. 48
      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. 39
      ui/components/app/menu-bar/account-options-menu.js
  80. 19
      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. 29
      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_VERSION='79.0.3945.117-1'
CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb" 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}" 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) (sudo dpkg -i "${CHROME_BINARY}" || sudo apt-get -fy install)
rm -rf "${CHROME_BINARY}" rm -rf "${CHROME_BINARY}"

@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Request a new feature - 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 about: Request new features and vote on the ones that are important to you
- name: Get support or ask a question - name: Get support or ask a question
url: https://metamask.zendesk.com/hc/en-us/requests/new url: https://metamask.zendesk.com/hc/en-us/requests/new

@ -9,6 +9,9 @@ jobs:
CLABot: CLABot:
if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/') if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps: steps:
- name: "CLA Signature Bot" - name: "CLA Signature Bot"
uses: MetaMask/cla-signature-bot@v3.0.2 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 React, { useEffect } from 'react';
import { addDecorator, addParameters } from '@storybook/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 { withKnobs } from '@storybook/addon-knobs';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import configureStore from '../ui/store/store'; import configureStore from '../ui/store/store';
@ -8,7 +8,11 @@ import '../ui/css/index.scss';
import localeList from '../app/_locales/index.json'; import localeList from '../app/_locales/index.json';
import * as allLocales from './locales'; import * as allLocales from './locales';
import { I18nProvider, LegacyI18nProvider } from './i18n'; import { I18nProvider, LegacyI18nProvider } from './i18n';
import MetaMetricsProviderStorybook from './metametrics'
import testData from './test-data.js'; import testData from './test-data.js';
import { Router } from "react-router-dom";
import { createBrowserHistory } from "history";
import { _setBackgroundConnection } from '../ui/store/actions'
addParameters({ addParameters({
backgrounds: { backgrounds: {
@ -41,13 +45,25 @@ const styles = {
alignItems: 'center', 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 metamaskDecorator = (story, context) => {
const currentLocale = context.globals.locale; const currentLocale = context.globals.locale;
const current = allLocales[currentLocale]; const current = allLocales[currentLocale];
return ( return (
<Provider store={store}> <Provider store={store}>
<Router history={history}>
<MetaMetricsProviderStorybook>
<I18nProvider <I18nProvider
currentLocale={currentLocale} currentLocale={currentLocale}
current={current} current={current}
@ -57,6 +73,8 @@ const metamaskDecorator = (story, context) => {
<div style={styles}>{story()}</div> <div style={styles}>{story()}</div>
</LegacyI18nProvider> </LegacyI18nProvider>
</I18nProvider> </I18nProvider>
</MetaMetricsProviderStorybook>
</Router>
</Provider> </Provider>
); );
}; };

@ -1,217 +1,782 @@
import { TRANSACTION_STATUSES } from '../shared/constants/transaction'; import { TRANSACTION_STATUSES } from '../shared/constants/transaction';
const state = { const state = {
metamask: { "invalidCustomNetwork": {
isInitialized: true, "state": "CLOSED",
isUnlocked: true, "networkName": ""
featureFlags: { sendHexData: true }, },
identities: { "unconnectedAccount": {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { "state": "CLOSED"
address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', },
name: 'Send Account 1', "activeTab": {},
}, "metamask": {
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { "isInitialized": true,
address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', "isUnlocked": true,
name: 'Send Account 2', "isAccountMenuOpen": false,
}, "rpcUrl": "https://rawtestrpc.metamask.io/",
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { "identities": {
address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', "0x983211ce699ea5ab57cc528086154b6db1ad8e55": {
name: 'Send Account 3', "name": "Account 1",
}, "address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55"
'0xd85a4b6a394794842887b8284293d69163007bbb': { },
address: '0xd85a4b6a394794842887b8284293d69163007bbb', "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {
name: 'Send Account 4', "name": "Account 2",
}, "address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e"
}, },
cachedBalances: {}, "0x9d0ba4ddac06032527b140912ec808ab9451b788": {
currentBlockGasLimit: '0x4c1878', "name": "Account 3",
currentCurrency: 'USD', "address": "0x9d0ba4ddac06032527b140912ec808ab9451b788"
conversionRate: 1200.88200327, }
conversionDate: 1489013762, },
nativeCurrency: 'ETH', "unapprovedTxs": {
frequentRpcList: [], "7786962153682822": {
network: '3', "id": 7786962153682822,
provider: { "time": 1620710815484,
type: 'ropsten', "status": "unapproved",
chainId: '0x3', "metamaskNetworkId": "3",
}, "chainId": "0x3",
accounts: { "loadingDefaults": false,
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { "txParams": {
code: '0x', "from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
balance: '0x47c9d71831c76efe', "to": "0xad6d458402f60fd3bd25163575031acdce07538d",
nonce: '0x1b', "value": "0x0",
address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', "data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000",
}, "gas": "0xcb28",
'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { "gasPrice": "0x77359400"
code: '0x', },
balance: '0x37452b1315889f80', "type": "standard",
nonce: '0xa', "origin": "metamask",
address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', "transactionCategory": "transfer",
}, "history": [
'0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { {
code: '0x', "id": 7786962153682822,
balance: '0x30c9d71831c76efe', "time": 1620710815484,
nonce: '0x1c', "status": "unapproved",
address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', "metamaskNetworkId": "3",
}, "chainId": "0x3",
'0xd85a4b6a394794842887b8284293d69163007bbb': { "loadingDefaults": true,
code: '0x', "txParams": {
balance: '0x0', "from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
nonce: '0x0', "to": "0xad6d458402f60fd3bd25163575031acdce07538d",
address: '0xd85a4b6a394794842887b8284293d69163007bbb', "value": "0x0",
}, "data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000",
}, "gas": "0xcb28",
addressBook: { "gasPrice": "0x77359400"
'0x3': { },
'0x06195827297c7a80a443b6894d3bdb8824b43896': { "type": "standard",
address: '0x06195827297c7a80a443b6894d3bdb8824b43896', "origin": "metamask",
name: 'Address Book Account 1', "transactionCategory": "transfer"
chainId: '0x3', },
}, [
}, {
}, "op": "replace",
tokens: [ "path": "/loadingDefaults",
{ "value": false,
address: '0x1a195821297c7a80a433b6894d3bdb8824b43896', "note": "Added new unapproved transaction.",
decimals: 18, "timestamp": 1620710815497
symbol: 'ABC', }
}, ]
{ ]
address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', }
decimals: 4, },
symbol: 'DEF', "frequentRpcList": [],
}, "addressBook": {
{ "undefined": {
address: '0xa42084c8d1d9a2198631988579bb36b48433a72b', "0": {
decimals: 18, "address": "0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0",
symbol: 'GHI', "name": "",
}, "isEns": false
}
}
},
"contractExchangeRates": {
"0xad6d458402f60fd3bd25163575031acdce07538d": 0
},
"tokens": [
{
"address": "0xad6d458402f60fd3bd25163575031acdce07538d",
"symbol": "DAI",
"decimals": 18
}
], ],
transactions: {}, "pendingTokens": {},
currentNetworkTxList: [ "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"
],
"keyrings": [
{ {
id: 'mockTokenTx1', "type": "HD Key Tree",
txParams: { "accounts": [
to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4",
from: '0xd85a4b6a394794842887b8284293d69163007bbb', "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e",
}, "0x9d0ba4ddac06032527b140912ec808ab9451b788"
time: 1700000000000, ]
}
],
"frequentRpcListDetail": [
{
"rpcUrl": "http://localhost:8545",
"chainId": "0x539",
"ticker": "ETH",
"nickname": "Localhost 8545",
"rpcPrefs": {}
}
],
"accountTokens": {
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": {
"0x1": [
{
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
"symbol": "DAI",
"decimals": 18
}, },
{ {
id: 'mockTokenTx2', "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
txParams: { "symbol": "BAT",
to: '0xafaketokenaddress', "decimals": 18
from: '0xd85a4b6a394794842887b8284293d69163007bbb', }
],
"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"
},
"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"
}, },
time: 1600000000000, {
"type": "filterResponse",
"value": [
"0x64a845a5b02460acf8a3d84503b0d68d028b4bb4"
],
"name": "exposedAccounts"
}
]
}
]
}
},
"permissionsLog": [
{
"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
},
"requestTime": 1602643170686,
"response": {
"id": 522690215,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1602643170688,
"success": true
}, },
{ {
id: 'mockTokenTx3', "id": 1620464600,
txParams: { "method": "eth_accounts",
to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', "methodType": "restricted",
from: '0xd85a4b6a394794842887b8284293d69163007bbb', "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": []
},
"responseTime": 1602643172935,
"success": true
}, },
time: 1500000000000, {
"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
},
"requestTime": 1620710669962,
"response": {
"id": 4279100021,
"jsonrpc": "2.0",
"result": []
},
"responseTime": 1620710669963,
"success": true
}, },
{ {
id: 'mockEthTx1', "id": 4279100022,
txParams: { "method": "eth_requestAccounts",
to: '0xd85a4b6a394794842887b8284293d69163007bbb', "methodType": "restricted",
from: '0xd85a4b6a394794842887b8284293d69163007bbb', "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
}, },
time: 1400000000000, {
"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
}
], ],
unapprovedMsgs: { "permissionsHistory": {
'0xabc': { id: 'unapprovedMessage1', time: 1650000000000 }, "https://app.uniswap.org": {
'0xdef': { id: 'unapprovedMessage2', time: 1550000000000 }, "eth_accounts": {
'0xghi': { id: 'unapprovedMessage3', time: 1450000000000 }, "lastApproved": 1620710693213,
}, "accounts": {
unapprovedMsgCount: 0, "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": 1620710693213
unapprovedPersonalMsgs: {}, }
unapprovedPersonalMsgCount: 0, }
unapprovedDecryptMsgs: {}, }
unapprovedDecryptMsgCount: 0, },
unapprovedEncryptionPublicKeyMsgs: {}, "domainMetadata": {
unapprovedEncryptionPublicKeyMsgCount: 0, "https://metamask.github.io": {
keyringTypes: ['Simple Key Pair', 'HD Key Tree'], "name": "E2E Test Dapp",
keyrings: [ "icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
{ "lastUpdated": 1620723443380,
type: 'HD Key Tree', "host": "metamask.github.io"
accounts: [ }
'fdea65c8e26263f6d9a1b5de9555d2931a33b825', },
'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', "threeBoxSyncingAllowed": false,
'2f8d4a878cfa04a6e60d46362f5644deab66572d', "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": {
"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": {}
},
"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": [
{ {
type: 'Simple Key Pair', "name": "_spender",
accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'], "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
}
], ],
selectedAddress: '0xd85a4b6a394794842887b8284293d69163007bbb', "payable": false,
send: { "stateMutability": "nonpayable",
gasLimit: '0xFFFF', "gas": null,
gasPrice: '0xaa', "_isFragment": true
gasTotal: '0xb451dc41b578', },
tokenBalance: 3434, "name": "approve",
from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', "signature": "approve(address,uint256)",
to: '0x987fedabc', "sighash": "0x095ea7b3",
amount: '0x080', "value": {
memo: '', "type": "BigNumber",
errors: { "hex": "0x00"
someError: null, }
}, },
maxModeOn: false, "fiatTransactionAmount": "0",
editingTransactionId: 97531, "fiatTransactionFee": "4.72",
}, "fiatTransactionTotal": "4.72",
unapprovedTxs: { "ethTransactionAmount": "0",
4768706228115573: { "ethTransactionFee": "0.0012",
id: 4768706228115573, "ethTransactionTotal": "0.0012",
time: 1487363153561, "hexTransactionAmount": "0x0",
status: TRANSACTION_STATUSES.UNAPPROVED, "hexTransactionFee": "0x44364c5bb0000",
gasMultiplier: 1, "hexTransactionTotal": "0x44364c5bb0000",
metamaskNetworkId: '3', "nonce": ""
txParams: { },
from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', "swaps": {
to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', "aggregatorMetadata": null,
value: '0xde0b6b3a7640000', "approveTxId": null,
metamaskId: 4768706228115573, "balanceError": false,
metamaskNetworkId: '3', "fetchingQuotes": false,
gas: '0x5209', "fromToken": null,
}, "quotesFetchStartTime": null,
txFee: '17e0186e60800', "topAssets": {},
txValue: 'de0b6b3a7640000', "toToken": null,
maxCost: 'de234b52e4a0800', "customGas": {
gasPrice: '4a817c800', "price": null,
}, "limit": null,
}, "loading": "INITIAL",
currentLocale: 'en', "priceEstimates": {},
}, "fallBackPrice": null
appState: { }
menuOpen: false, },
currentView: { "gas": {
name: 'accountDetail', "customData": {
detailView: null, "price": null,
context: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', "limit": "0xcb28"
}, },
accountDetail: { "basicEstimates": {
subview: 'transactions', "average": 2
}, },
modal: { "basicEstimateIsLoading": false
modalState: {}, }
previousModalState: {}, }
},
isLoading: false,
warning: null,
scrollToBottom: false,
forgottenPassword: null,
},
send: {
fromDropdownOpen: false,
toDropdownOpen: false,
errors: { someError: null },
},
};
export default state; export default state;

@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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] ## [9.6.1]
### Fixed ### Fixed
- [#11309](https://github.com/MetaMask/metamask-extension/pull/11309): Fixed signTypeData parameter validation issue - [#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 ### Uncategorized
- Added the ability to restore accounts from seed words. - 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.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.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 [9.5.9]: https://github.com/MetaMask/metamask-extension/compare/v9.5.8...v9.5.9

@ -52,6 +52,10 @@
"addContact": { "addContact": {
"message": "Add contact" "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": { "addEthereumChainConfirmationDescription": {
"message": "This will allow this network to be used within MetaMask." "message": "This will allow this network to be used within MetaMask."
}, },
@ -285,6 +289,9 @@
"chainIdDefinition": { "chainIdDefinition": {
"message": "The chain ID used to sign transactions for this network." "message": "The chain ID used to sign transactions for this network."
}, },
"chainIdExistsErrorMsg": {
"message": "This Chain ID is currently used by the $1 network."
},
"chromeRequiredForHardwareWallets": { "chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
}, },
@ -410,6 +417,9 @@
"continueToWyre": { "continueToWyre": {
"message": "Continue to Wyre" "message": "Continue to Wyre"
}, },
"contract": {
"message": "Contract"
},
"contractAddressError": { "contractAddressError": {
"message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens." "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", "message": "or $1",
"description": "$1 represents the text from `importAccountLinkText` as a link" "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": { "importWallet": {
"message": "Import wallet" "message": "Import wallet"
}, },
@ -1042,6 +1058,9 @@
"mainnet": { "mainnet": {
"message": "Ethereum Mainnet" "message": "Ethereum Mainnet"
}, },
"makeAnotherSwap": {
"message": "Create a new swap"
},
"max": { "max": {
"message": "Max" "message": "Max"
}, },
@ -1438,6 +1457,30 @@
"recipientAddressPlaceholder": { "recipientAddressPlaceholder": {
"message": "Search, public address (0x), or ENS" "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": { "reject": {
"message": "Reject" "message": "Reject"
}, },
@ -1949,18 +1992,18 @@
"message": "You are about to swap $1 $2 (~$3) for $4 $5 (~$6).", "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." "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": { "swapPriceDifferenceTitle": {
"message": "Price difference of ~$1%", "message": "Price difference of ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceDifferenceTooltip": { "swapPriceImpactTooltip": {
"message": "The difference in market prices can be affected by fees taken by intermediaries, size of market, size of trade, or market inefficiencies." "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": { "swapPriceUnavailableTitle": {
"message": "Market price is unavailable. Make sure you feel comfortable with the returned amount before proceeding." "message": "Check your rate before proceeding"
}, },
"swapProcessing": { "swapProcessing": {
"message": "Processing" "message": "Processing"
@ -2063,13 +2106,13 @@
"message": "Swap $1 to $2", "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." "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": { "swapTokenVerificationMessage": {
"message": "Always confirm the token address on $1.", "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." "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": { "swapTokenVerificationOnlyOneSource": {
"message": "Only verified on 1 source." "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.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 available to swap", "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" "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": { "tokenSymbol": {
"message": "Token Symbol" "message": "Token Symbol"
}, },
"tooltipApproveButton": {
"message": "I understand"
},
"total": { "total": {
"message": "Total" "message": "Total"
}, },
@ -2333,7 +2376,7 @@
"message": "URLs require the appropriate HTTP/HTTPS prefix." "message": "URLs require the appropriate HTTP/HTTPS prefix."
}, },
"urlExistsErrorMsg": { "urlExistsErrorMsg": {
"message": "URL is already present in existing list of networks" "message": "This URL is currently used by the $1 network."
}, },
"usePhishingDetection": { "usePhishingDetection": {
"message": "Use Phishing Detection" "message": "Use Phishing Detection"
@ -2355,6 +2398,10 @@
"message": "Verify this token on $1", "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\"" "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": { "viewAccount": {
"message": "View Account" "message": "View Account"
}, },

@ -1034,6 +1034,9 @@
"mainnet": { "mainnet": {
"message": "Red principal de Ethereum" "message": "Red principal de Ethereum"
}, },
"makeAnotherSwap": {
"message": "Crear un nuevo canje"
},
"max": { "max": {
"message": "Máx." "message": "Máx."
}, },
@ -1893,12 +1896,6 @@
"message": "Diferencia de precio de ~$1 %", "message": "Diferencia de precio de ~$1 %",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Procesamiento" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 disponible para canje", "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" "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": { "tokenSymbol": {
"message": "Símbolo del token" "message": "Símbolo del token"
}, },
"tooltipApproveButton": {
"message": "Comprendo"
},
"total": { "total": {
"message": "Total" "message": "Total"
}, },

@ -1042,6 +1042,9 @@
"mainnet": { "mainnet": {
"message": "Red principal de Ethereum" "message": "Red principal de Ethereum"
}, },
"makeAnotherSwap": {
"message": "Crear un nuevo canje"
},
"max": { "max": {
"message": "Máx." "message": "Máx."
}, },
@ -1937,12 +1940,6 @@
"message": "Diferencia de precio de ~$1 %", "message": "Diferencia de precio de ~$1 %",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Procesamiento" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 disponible para canje", "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" "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": { "tokenSymbol": {
"message": "Símbolo del token" "message": "Símbolo del token"
}, },
"tooltipApproveButton": {
"message": "Comprendo"
},
"total": { "total": {
"message": "Total" "message": "Total"
}, },

@ -1034,6 +1034,9 @@
"mainnet": { "mainnet": {
"message": "Ethereum Mainnet" "message": "Ethereum Mainnet"
}, },
"makeAnotherSwap": {
"message": "एक नयप बन"
},
"max": { "max": {
"message": "अधिकतम" "message": "अधिकतम"
}, },
@ -1893,6 +1896,15 @@
"message": "~$1% कय अतर", "message": "~$1% कय अतर",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceImpactTooltip": {
"message": "मय परभव, वरतमन बर मय और लन-दन निदन कन पत रिच कतर ह। मय परभव चलनििल क आकर कष आपकर क आकर क एक कय ह।"
},
"swapPriceUnavailableDescription": {
"message": "बर मय ड कमरण मय परभव किित नहि सक। कपयि करि आप सप करन पहलत हकन किकर सहज ह।"
},
"swapPriceUnavailableTitle": {
"message": "आग बढ पहल अपन दर कच कर"
},
"swapProcessing": { "swapProcessing": {
"message": "परसकरण" "message": "परसकरण"
}, },
@ -2021,9 +2033,6 @@
"message": "एकिक टकन एक हम और परतक क उपयग कर सकत। यह सतित करनिए $1 कच करि यह वहकन ह, जिसक आप तलश कर रह।", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 सप किए उपलबध ह", "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" "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": { "tokenSymbol": {
"message": "टकन करतक" "message": "टकन करतक"
}, },
"tooltipApproveButton": {
"message": "म समझत"
},
"total": { "total": {
"message": "कलयग" "message": "कलयग"
}, },

@ -1034,6 +1034,9 @@
"mainnet": { "mainnet": {
"message": "Ethereum Mainnet" "message": "Ethereum Mainnet"
}, },
"makeAnotherSwap": {
"message": "Buat penukaran baru"
},
"max": { "max": {
"message": "Maks." "message": "Maks."
}, },
@ -1893,6 +1896,15 @@
"message": "Perbedaan harga ~$1%", "message": "Perbedaan harga ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Memproses" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 tersedia untuk ditukar", "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" "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": { "tokenSymbol": {
"message": "Simbol Token" "message": "Simbol Token"
}, },
"tooltipApproveButton": {
"message": "Saya paham"
},
"total": { "total": {
"message": "Total" "message": "Total"
}, },

@ -1621,19 +1621,10 @@
"message": "Stai per scambiare $1 $2 (~$3) per $4 $5 (~$6).", "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." "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": { "swapPriceDifferenceTitle": {
"message": "Differenza di prezzo di circa ~$1%", "message": "Differenza di prezzo di circa ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "In elaborazione" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 disponibili allo scambio", "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" "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": { "mainnet": {
"message": "イーサリアム メインネット" "message": "イーサリアム メインネット"
}, },
"makeAnotherSwap": {
"message": "新しいスワップの作成"
},
"max": { "max": {
"message": "最大" "message": "最大"
}, },
@ -1893,12 +1896,6 @@
"message": "約 $1% の価格差", "message": "約 $1% の価格差",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceDifferenceTooltip": {
"message": "市場価格の違いは、仲介業者が負担する手数料、市場規模、取引量、または取引価格差の影響を受けることがあります。"
},
"swapPriceDifferenceUnavailable": {
"message": "マーケット価格は利用できません。続行する前に、返金額に問題がないことを確認してください。"
},
"swapProcessing": { "swapProcessing": {
"message": "処理中" "message": "処理中"
}, },
@ -2027,9 +2024,6 @@
"message": "複数のトークンが同じ名前とシンボルを使用できます。$1 をチェックして、これが探しているトークンであることを確認します。", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 はスワップに使用可能です", "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" "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": { "tokenSymbol": {
"message": "トークン シンボル" "message": "トークン シンボル"
}, },
"tooltipApproveButton": {
"message": "理解しました"
},
"total": { "total": {
"message": "合計" "message": "合計"
}, },

@ -1038,6 +1038,9 @@
"mainnet": { "mainnet": {
"message": "이더리움 메인넷" "message": "이더리움 메인넷"
}, },
"makeAnotherSwap": {
"message": "새 스왑 생성"
},
"max": { "max": {
"message": "최대" "message": "최대"
}, },
@ -1933,6 +1936,15 @@
"message": "~$1%의 가격 차이", "message": "~$1%의 가격 차이",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceImpactTooltip": {
"message": "가격 영향은 현재 시장 가격과 거래 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 거래의 크기를 나타내는 함수입니다."
},
"swapPriceUnavailableDescription": {
"message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수에 만족하시는지 확인하시기 바랍니다."
},
"swapPriceUnavailableTitle": {
"message": "진행하기 전에 요율을 확인하십시오."
},
"swapProcessing": { "swapProcessing": {
"message": "처리 중" "message": "처리 중"
}, },
@ -2061,9 +2073,6 @@
"message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1을(를) 확인하여 원하는 토큰인지 확인하세요.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 스왑 가능", "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" "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": { "tokenSymbol": {
"message": "토큰 기호" "message": "토큰 기호"
}, },
"tooltipApproveButton": {
"message": "이해했습니다."
},
"total": { "total": {
"message": "합계" "message": "합계"
}, },

@ -1042,6 +1042,9 @@
"mainnet": { "mainnet": {
"message": "Ethereum Mainnet" "message": "Ethereum Mainnet"
}, },
"makeAnotherSwap": {
"message": "Gumawa ng bagong swap"
},
"max": { "max": {
"message": "Max" "message": "Max"
}, },
@ -1937,6 +1940,15 @@
"message": "Kaibahan sa presyo na ~$1%", "message": "Kaibahan sa presyo na ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Pagproseso" "message": "Pagproseso"
}, },
@ -2188,6 +2200,9 @@
"tokenSymbol": { "tokenSymbol": {
"message": "Simbolo ng Token" "message": "Simbolo ng Token"
}, },
"tooltipApproveButton": {
"message": "Nauunawaan ko"
},
"total": { "total": {
"message": "Kabuuan" "message": "Kabuuan"
}, },

@ -1028,6 +1028,9 @@
"mainnet": { "mainnet": {
"message": "Mainnet do Ethereum" "message": "Mainnet do Ethereum"
}, },
"makeAnotherSwap": {
"message": "Criar novo swap"
},
"max": { "max": {
"message": "Máx" "message": "Máx"
}, },
@ -1877,6 +1880,15 @@
"message": "Diferença de preço de aproximadamente $1%", "message": "Diferença de preço de aproximadamente $1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Processando" "message": "Processando"
}, },
@ -2128,6 +2140,9 @@
"tokenSymbol": { "tokenSymbol": {
"message": "Símbolo do token" "message": "Símbolo do token"
}, },
"tooltipApproveButton": {
"message": "Eu entendo"
},
"total": { "total": {
"message": "Total" "message": "Total"
}, },

@ -1034,6 +1034,9 @@
"mainnet": { "mainnet": {
"message": "Сеть Ethereum Mainnet" "message": "Сеть Ethereum Mainnet"
}, },
"makeAnotherSwap": {
"message": "Создать новый своп"
},
"max": { "max": {
"message": "Макс." "message": "Макс."
}, },
@ -1893,6 +1896,15 @@
"message": "Разница в цене составляет ~$1%", "message": "Разница в цене составляет ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceImpactTooltip": {
"message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время выполнения транзакции. Колебание цены зависит от размера вашей сделки относительно размера пула ликвидности."
},
"swapPriceUnavailableDescription": {
"message": "Колебание цены определить не удалось из-за отсутствия данных о рыночных ценах. Перед свопом подтвердите, что вас устраивает количество токенов, которое вы получите."
},
"swapPriceUnavailableTitle": {
"message": "Прежде чем продолжить, проверьте курс"
},
"swapProcessing": { "swapProcessing": {
"message": "Обработка" "message": "Обработка"
}, },
@ -2021,9 +2033,6 @@
"message": "Несколько токенов могут использовать одно и то же имя и символ. Убедитесь, что это именно тот токен, который вы ищете, на $1.", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 доступны для свопа", "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" "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": { "tokenSymbol": {
"message": "Символ токена" "message": "Символ токена"
}, },
"tooltipApproveButton": {
"message": "Я понимаю"
},
"total": { "total": {
"message": "Итого" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "Available ang $1 $2 na i-swap", "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" "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": { "mainnet": {
"message": "Mạng chính thức của Ethereum" "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": { "max": {
"message": "Tối đa" "message": "Tối đa"
}, },
@ -1893,6 +1896,15 @@
"message": "Chênh lệch giá ~$1%", "message": "Chênh lệch giá ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "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": { "swapProcessing": {
"message": "Đang xử lý" "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.", "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." "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": { "swapYourTokenBalance": {
"message": "Có sẵn $1 $2 để hoán đổi", "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" "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": { "tokenSymbol": {
"message": "Ký hiệu token" "message": "Ký hiệu token"
}, },
"tooltipApproveButton": {
"message": "Tôi đã hiểu"
},
"total": { "total": {
"message": "Tổng" "message": "Tổng"
}, },

@ -1619,12 +1619,6 @@
"message": "价格差异 ~$1%", "message": "价格差异 ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference." "description": "$1 is a number (ex: 1.23) that represents the price difference."
}, },
"swapPriceDifferenceTooltip": {
"message": "市场价格的差异可能受到中介机构收取的费用、市场规模、交易规模或市场效率低下的影响。"
},
"swapPriceDifferenceUnavailable": {
"message": "市场价格不可用。 请确认您对退回的数额感到满意后再继续。"
},
"swapProcessing": { "swapProcessing": {
"message": "处理中" "message": "处理中"
}, },
@ -1726,9 +1720,6 @@
"message": "多个代币可以使用相同的名称和符号。检查 $1(以太坊浏览器)以确认这是您正在寻找的代币。", "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." "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": { "swapYourTokenBalance": {
"message": "$1 $2 可用", "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" "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_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../shared/constants/app'; } from '../../shared/constants/app';
import { SECOND } from '../../shared/constants/time';
import migrations from './migrations'; import migrations from './migrations';
import Migrator from './lib/migrator'; import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension'; import ExtensionPlatform from './platforms/extension';
@ -491,7 +492,7 @@ async function openPopup() {
clearInterval(interval); clearInterval(interval);
resolve(); resolve();
} }
}, 1000); }, SECOND);
}); });
} }

@ -1,6 +1,7 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import { MINUTE } from '../../../shared/constants/time';
export default class AppStateController extends EventEmitter { export default class AppStateController extends EventEmitter {
/** /**
@ -24,6 +25,8 @@ export default class AppStateController extends EventEmitter {
connectedStatusPopoverHasBeenShown: true, connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: null, defaultHomeActiveTabName: null,
browserEnvironment: {}, browserEnvironment: {},
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(),
...initState, ...initState,
}); });
this.timer = null; 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 * Sets the last active time to the current time
* @returns {void} * @returns {void}
@ -156,7 +180,7 @@ export default class AppStateController extends EventEmitter {
this.timer = setTimeout( this.timer = setTimeout(
() => this.onInactiveTimeout(), () => 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 SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
import { MINUTE } from '../../../shared/constants/time';
// By default, poll every 3 minutes // By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000; const DEFAULT_INTERVAL = MINUTE * 3;
/** /**
* A controller that polls for token exchange * A controller that polls for token exchange

@ -18,8 +18,9 @@ import {
RINKEBY_CHAIN_ID, RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID, ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network'; } 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 * @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta

@ -19,6 +19,7 @@ import {
TRANSACTION_TYPES, TRANSACTION_TYPES,
TRANSACTION_STATUSES, TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { MILLISECOND } from '../../../shared/constants/time';
const IncomingTransactionsController = proxyquire('./incoming-transactions', { const IncomingTransactionsController = proxyquire('./incoming-transactions', {
'../../../shared/modules/random-id': { default: () => 54321 }, '../../../shared/modules/random-id': { default: () => 54321 },
@ -26,7 +27,7 @@ const IncomingTransactionsController = proxyquire('./incoming-transactions', {
const FAKE_CHAIN_ID = '0x1338'; const FAKE_CHAIN_ID = '0x1338';
const MOCK_SELECTED_ADDRESS = '0x0101'; 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 EXISTING_INCOMING_TX = { id: 777, hash: '0x123456' };
const PREPOPULATED_INCOMING_TXS_BY_HASH = { 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 createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'; import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import { PollingBlockTracker } from 'eth-block-tracker'; import { PollingBlockTracker } from 'eth-block-tracker';
import { SECOND } from '../../../../shared/constants/time';
const inTest = process.env.IN_TEST === 'true'; const inTest = process.env.IN_TEST === 'true';
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {}; const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {};
const getTestMiddlewares = () => { const getTestMiddlewares = () => {
return inTest ? [createEstimateGasDelayTestMiddleware()] : []; return inTest ? [createEstimateGasDelayTestMiddleware()] : [];
}; };
@ -51,7 +52,7 @@ function createChainIdMiddleware(chainId) {
function createEstimateGasDelayTestMiddleware() { function createEstimateGasDelayTestMiddleware() {
return createAsyncMiddleware(async (req, _, next) => { return createAsyncMiddleware(async (req, _, next) => {
if (req.method === 'eth_estimateGas') { if (req.method === 'eth_estimateGas') {
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
} }
return next(); return next();
}); });

@ -19,6 +19,7 @@ import {
RINKEBY_CHAIN_ID, RINKEBY_CHAIN_ID,
INFURA_BLOCKED_KEY, INFURA_BLOCKED_KEY,
} from '../../../../shared/constants/network'; } from '../../../../shared/constants/network';
import { SECOND } from '../../../../shared/constants/time';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
@ -29,7 +30,7 @@ import createInfuraClient from './createInfuraClient';
import createJsonRpcClient from './createJsonRpcClient'; import createJsonRpcClient from './createJsonRpcClient';
const env = process.env.METAMASK_ENV; const env = process.env.METAMASK_ENV;
const fetchWithTimeout = getFetchWithTimeout(30000); const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
let defaultProviderConfigOpts; let defaultProviderConfigOpts;
if (process.env.IN_TEST === 'true') { 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( assert.notStrictEqual(
type, type,
NETWORK_TYPE_RPC, NETWORK_TYPE_RPC,
@ -216,7 +217,13 @@ export default class NetworkController extends EventEmitter {
`Unknown Infura provider type "${type}".`, `Unknown Infura provider type "${type}".`,
); );
const { chainId } = NETWORK_TYPE_TO_ID_MAP[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() { resetConnection() {

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

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

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

@ -3,11 +3,12 @@ import log from 'loglevel';
import { normalize as normalizeAddress } from 'eth-sig-util'; import { normalize as normalizeAddress } from 'eth-sig-util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; 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 // By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000; const DEFAULT_INTERVAL = MINUTE * 3;
/** /**
* A controller that polls for token exchange * A controller that polls for token exchange

@ -23,6 +23,7 @@ import {
TRANSACTION_TYPES, TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction'; } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import TransactionStateManager from './tx-state-manager'; import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils'; import TxGasUtil from './tx-gas-utils';
import PendingTransactionTracker from './pending-tx-tracker'; import PendingTransactionTracker from './pending-tx-tracker';
@ -30,7 +31,6 @@ import * as txUtils from './lib/util';
const hstInterface = new ethers.utils.Interface(abi); 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 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 // This is a standard ether simple send, gas requirement is exactly 21k
return { gasLimit: SIMPLE_GAS_COST }; return { gasLimit: GAS_LIMITS.SIMPLE };
} }
const { const {
@ -404,7 +404,7 @@ export default class TransactionController extends EventEmitter {
from, from,
to: from, to: from,
nonce, nonce,
gas: customGasLimit || '0x5208', gas: customGasLimit || GAS_LIMITS.SIMPLE,
value: '0x0', value: '0x0',
gasPrice: newGasPrice, gasPrice: newGasPrice,
}, },

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

@ -1,18 +1,36 @@
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
/**
* @typedef {import('@metamask/controllers').ControllerMessenger} ControllerMessenger
*/
/** /**
* An ObservableStore that can composes a flat * An ObservableStore that can composes a flat
* structure of child stores based on configuration * structure of child stores based on configuration
*/ */
export default class ComposableObservableStore extends ObservableStore { 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 * Create a new store
* *
* @param {Object} [initState] - The initial store state * @param {Object} options
* @param {Object} [config] - Map of internal state keys to child stores * @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) { constructor({ config, controllerMessenger, state }) {
super(initState); super(state);
this.controllerMessenger = controllerMessenger;
if (config) { if (config) {
this.updateStructure(config); this.updateStructure(config);
} }
@ -21,15 +39,31 @@ export default class ComposableObservableStore extends ObservableStore {
/** /**
* Composes a new internal store subscription structure * 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) { updateStructure(config) {
this.config = config; this.config = config;
this.removeAllListeners(); this.removeAllListeners();
for (const key of Object.keys(this.config)) { 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) => { config[key].subscribe((state) => {
this.updateState({ [key]: 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 { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import {
BaseController,
BaseControllerV2,
ControllerMessenger,
} from '@metamask/controllers';
import ComposableObservableStore from './ComposableObservableStore'; 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 () { describe('ComposableObservableStore', function () {
it('should register initial state', 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'); assert.strictEqual(store.getState(), 'state');
}); });
it('should register initial structure', function () { it('should register initial structure', function () {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore(); const testStore = new ObservableStore();
const store = new ComposableObservableStore(null, { TestStore: testStore }); const store = new ComposableObservableStore({
config: { TestStore: testStore },
controllerMessenger,
});
testStore.putState('state'); testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: '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 testStore = new ObservableStore();
const store = new ComposableObservableStore(); const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ TestStore: testStore }); store.updateStructure({ TestStore: testStore });
testStore.putState('state'); testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: '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 () { it('should return flattened state', function () {
const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' }); const fooStore = new ObservableStore({ foo: 'foo' });
const barStore = new ObservableStore({ bar: 'bar' }); const barController = new ExampleController({
const store = new ComposableObservableStore(null, { messenger: controllerMessenger,
});
const bazController = new OldExampleController();
const store = new ComposableObservableStore({
config: {
FooStore: fooStore, FooStore: fooStore,
BarStore: barStore, 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 () { 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(), {}); 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 extension from 'extensionizer';
import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout';
import { SECOND } from '../../../../shared/constants/time';
import resolveEnsToIpfsContentId from './resolver'; import resolveEnsToIpfsContentId from './resolver';
const fetchWithTimeout = getFetchWithTimeout(30000); const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
const supportedTopLevelDomains = ['eth']; const supportedTopLevelDomains = ['eth'];

@ -1,7 +1,8 @@
import log from 'loglevel'; import log from 'loglevel';
import { SECOND } from '../../../shared/constants/time';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; 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_HOST = 'localhost';
const FIXTURE_SERVER_PORT = 12345; const FIXTURE_SERVER_PORT = 12345;

@ -1,4 +1,5 @@
import Analytics from 'analytics-node'; import Analytics from 'analytics-node';
import { SECOND } from '../../../shared/constants/time';
const isDevOrTestEnvironment = Boolean( const isDevOrTestEnvironment = Boolean(
process.env.METAMASK_DEBUG || process.env.IN_TEST, 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 // 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 // e.g confirmations. This is set to 5,000ms (5 seconds) arbitrarily with the
// intent of having a value less than 10 seconds. // 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 * Creates a mock segment module for usage in test environments. This is used

@ -20,6 +20,7 @@ import contractMap from '@metamask/contract-metadata';
import { import {
AddressBookController, AddressBookController,
ApprovalController, ApprovalController,
ControllerMessenger,
CurrencyRateController, CurrencyRateController,
PhishingController, PhishingController,
NotificationController, NotificationController,
@ -28,6 +29,7 @@ import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network';
import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { UI_NOTIFICATIONS } from '../../shared/notifications';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { MILLISECOND } from '../../shared/constants/time';
import ComposableObservableStore from './lib/ComposableObservableStore'; import ComposableObservableStore from './lib/ComposableObservableStore';
import AccountTracker from './lib/account-tracker'; import AccountTracker from './lib/account-tracker';
@ -81,7 +83,10 @@ export default class MetamaskController extends EventEmitter {
this.defaultMaxListeners = 20; this.defaultMaxListeners = 20;
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200); this.sendUpdate = debounce(
this.privateSendUpdate.bind(this),
MILLISECOND * 200,
);
this.opts = opts; this.opts = opts;
this.extension = opts.extension; this.extension = opts.extension;
this.platform = opts.platform; this.platform = opts.platform;
@ -96,8 +101,13 @@ export default class MetamaskController extends EventEmitter {
this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getRequestAccountTabIds = opts.getRequestAccountTabIds;
this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds;
const controllerMessenger = new ControllerMessenger();
// observable state store // observable state store
this.store = new ComposableObservableStore(initState); this.store = new ComposableObservableStore({
state: initState,
controllerMessenger,
});
// external connections by origin // external connections by origin
// Do not modify directly. Use the associated methods. // Do not modify directly. Use the associated methods.
@ -157,10 +167,14 @@ export default class MetamaskController extends EventEmitter {
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
}); });
this.currencyRateController = new CurrencyRateController( const currencyRateMessenger = controllerMessenger.getRestricted({
{ includeUSDRate: true }, name: 'CurrencyRateController',
initState.CurrencyController, });
); this.currencyRateController = new CurrencyRateController({
includeUSDRate: true,
messenger: currencyRateMessenger,
state: initState.CurrencyController,
});
this.phishingController = new PhishingController(); this.phishingController = new PhishingController();
@ -222,10 +236,12 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.start(); this.accountTracker.start();
this.incomingTransactionsController.start(); this.incomingTransactionsController.start();
this.tokenRatesController.start(); this.tokenRatesController.start();
this.currencyRateController.start();
} else { } else {
this.accountTracker.stop(); this.accountTracker.stop();
this.incomingTransactionsController.stop(); this.incomingTransactionsController.stop();
this.tokenRatesController.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.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => {
this.setCurrentCurrency( const { ticker } = this.networkController.getProviderConfig();
this.currencyRateController.state.currentCurrency, try {
(error) => { await this.currencyRateController.setNativeCurrency(ticker);
if (error) { } catch (error) {
throw 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.networkController.lookupNetwork();
this.messageManager = new MessageManager(); this.messageManager = new MessageManager();
this.personalMessageManager = new PersonalMessageManager(); this.personalMessageManager = new PersonalMessageManager();
@ -439,7 +452,8 @@ export default class MetamaskController extends EventEmitter {
NotificationController: this.notificationController, NotificationController: this.notificationController,
}); });
this.memStore = new ComposableObservableStore(null, { this.memStore = new ComposableObservableStore({
config: {
AppStateController: this.appStateController.store, AppStateController: this.appStateController.store,
NetworkController: this.networkController.store, NetworkController: this.networkController.store,
AccountTracker: this.accountTracker.store, AccountTracker: this.accountTracker.store,
@ -458,7 +472,8 @@ export default class MetamaskController extends EventEmitter {
CurrencyController: this.currencyRateController, CurrencyController: this.currencyRateController,
AlertController: this.alertController.store, AlertController: this.alertController.store,
OnboardingController: this.onboardingController.store, OnboardingController: this.onboardingController.store,
IncomingTransactionsController: this.incomingTransactionsController.store, IncomingTransactionsController: this.incomingTransactionsController
.store,
PermissionsController: this.permissionsController.permissions, PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store, PermissionsMetadata: this.permissionsController.store,
ThreeBoxController: this.threeBoxController.store, ThreeBoxController: this.threeBoxController.store,
@ -466,6 +481,8 @@ export default class MetamaskController extends EventEmitter {
EnsController: this.ensController.store, EnsController: this.ensController.store,
ApprovalController: this.approvalController, ApprovalController: this.approvalController,
NotificationController: this.notificationController, NotificationController: this.notificationController,
},
controllerMessenger,
}); });
this.memStore.subscribe(this.sendUpdate.bind(this)); this.memStore.subscribe(this.sendUpdate.bind(this));
@ -649,7 +666,11 @@ export default class MetamaskController extends EventEmitter {
return { return {
// etc // etc
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this), setCurrentCurrency: nodeify(
this.currencyRateController.setCurrentCurrency.bind(
this.currencyRateController,
),
),
setUseBlockie: this.setUseBlockie.bind(this), setUseBlockie: this.setUseBlockie.bind(this),
setUseNonceField: this.setUseNonceField.bind(this), setUseNonceField: this.setUseNonceField.bind(this),
setUsePhishDetect: this.setUsePhishDetect.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this),
@ -763,6 +784,14 @@ export default class MetamaskController extends EventEmitter {
this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController.setConnectedStatusPopoverHasBeenShown,
this.appStateController, this.appStateController,
), ),
setRecoveryPhraseReminderHasBeenShown: nodeify(
this.appStateController.setRecoveryPhraseReminderHasBeenShown,
this.appStateController,
),
setRecoveryPhraseReminderLastShown: nodeify(
this.appStateController.setRecoveryPhraseReminderLastShown,
this.appStateController,
),
// EnsController // EnsController
tryReverseResolveAddress: nodeify( tryReverseResolveAddress: nodeify(
@ -2511,29 +2540,6 @@ export default class MetamaskController extends EventEmitter {
// Log blocks // 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 * 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. * @param {string} rpcUrl - A URL for a valid Ethereum RPC API.

@ -654,46 +654,24 @@ describe('MetaMaskController', function () {
}); });
describe('#setCustomRpc', function () { describe('#setCustomRpc', function () {
let rpcUrl; it('returns custom RPC that when called', async function () {
const rpcUrl = await metamaskController.setCustomRpc(
beforeEach(function () {
rpcUrl = metamaskController.setCustomRpc(
CUSTOM_RPC_URL, CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID, CUSTOM_RPC_CHAIN_ID,
); );
assert.equal(rpcUrl, CUSTOM_RPC_URL);
}); });
it('returns custom RPC that when called', async function () { it('changes the network controller rpc', async function () {
assert.equal(await rpcUrl, CUSTOM_RPC_URL); await metamaskController.setCustomRpc(
}); CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID,
it('changes the network controller rpc', function () { );
const networkControllerState = metamaskController.networkController.store.getState(); const networkControllerState = metamaskController.networkController.store.getState();
assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL); 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 () { describe('#addNewAccount', function () {
it('errors when an primary keyring is does not exist', async function () { it('errors when an primary keyring is does not exist', async function () {
const addNewAccount = metamaskController.addNewAccount(); 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('./058').default,
require('./059').default, require('./059').default,
require('./060').default, require('./060').default,
require('./061').default,
]; ];
export default migrations; export default migrations;

@ -1,8 +1,8 @@
import extension from 'extensionizer'; import extension from 'extensionizer';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { getEnvironmentType, checkForError } from '../lib/util'; import { getEnvironmentType, checkForError } from '../lib/util';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import { getBlockExplorerUrlForTx } from '../../../shared/modules/transaction.utils';
export default class ExtensionPlatform { export default class ExtensionPlatform {
// //
@ -192,7 +192,7 @@ export default class ExtensionPlatform {
_showConfirmedTransaction(txMeta, rpcPrefs) { _showConfirmedTransaction(txMeta, rpcPrefs) {
this._subscribeToNotificationClicked(); this._subscribeToNotificationClicked();
const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs); const url = getBlockExplorerLink(txMeta, rpcPrefs);
const nonce = parseInt(txMeta.txParams.nonce, 16); const nonce = parseInt(txMeta.txParams.nonce, 16);
const title = 'Confirmed transaction'; 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 #!/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 VERSION = require('../dist/chrome/manifest.json').version; // eslint-disable-line import/no-unresolved
const { runCommand, runInShell } = require('./lib/run-command');
start().catch((error) => { start().catch((error) => {
console.error(error); console.error(error);
@ -31,11 +28,17 @@ async function start() {
} else { } else {
// create sentry release // create sentry release
console.log(`creating Sentry release for "${VERSION}"...`); console.log(`creating Sentry release for "${VERSION}"...`);
await exec(`sentry-cli releases new ${VERSION}`); await runCommand('sentry-cli', ['releases', 'new', VERSION]);
console.log( console.log(
`removing any existing files from Sentry release "${VERSION}"...`, `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 // check if version has artifacts or not
@ -49,34 +52,43 @@ async function start() {
} }
// upload sentry source and sourcemaps // 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() { async function checkIfAuthWorks() {
const itWorked = await doesNotFail(async () => { return await doesNotFail(() =>
await exec(`sentry-cli releases list`); runCommand('sentry-cli', ['releases', 'list']),
}); );
return itWorked;
} }
async function checkIfVersionExists() { async function checkIfVersionExists() {
const versionAlreadyExists = await doesNotFail(async () => { return await doesNotFail(() =>
await exec(`sentry-cli releases info ${VERSION}`); runCommand('sentry-cli', ['releases', 'info', VERSION]),
}); );
return versionAlreadyExists;
} }
async function checkIfVersionHasArtifacts() { 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 ['', ''] // 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) { async function doesNotFail(asyncFn) {
try { try {
await asyncFn(); await asyncFn();
return true; return true;
} catch (err) { } catch (error) {
if (error.message === `Exited with code '1'`) {
return false; return false;
} }
throw error;
}
} }

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

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

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

@ -1,10 +1,10 @@
{ {
"name": "metamask-crx", "name": "metamask-crx",
"version": "9.6.1", "version": "9.7.0",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/MetaMask/metamask-extension" "url": "https://github.com/MetaMask/metamask-extension.git"
}, },
"scripts": { "scripts": {
"setup": "yarn install && yarn setup:postinstall", "setup": "yarn install && yarn setup:postinstall",
@ -96,8 +96,8 @@
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"@lavamoat/preinstall-always-fail": "^1.0.0", "@lavamoat/preinstall-always-fail": "^1.0.0",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.22.0", "@metamask/contract-metadata": "^1.26.0",
"@metamask/controllers": "^8.0.0", "@metamask/controllers": "^9.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.5.0", "@metamask/eth-ledger-bridge-keyring": "^0.5.0",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",
@ -138,7 +138,7 @@
"ethereum-ens-network-map": "^1.0.2", "ethereum-ens-network-map": "^1.0.2",
"ethereumjs-abi": "^0.6.4", "ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "1.3.7", "ethereumjs-tx": "1.3.7",
"ethereumjs-util": "^7.0.9", "ethereumjs-util": "^7.0.10",
"ethereumjs-wallet": "^0.6.4", "ethereumjs-wallet": "^0.6.4",
"ethers": "^5.0.8", "ethers": "^5.0.8",
"ethjs": "^0.4.0", "ethjs": "^0.4.0",
@ -212,7 +212,7 @@
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/register": "^7.5.5", "@babel/register": "^7.5.5",
"@lavamoat/allow-scripts": "^1.0.6", "@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": "^6.0.0",
"@metamask/eslint-config-jest": "^6.0.0", "@metamask/eslint-config-jest": "^6.0.0",
"@metamask/eslint-config-mocha": "^6.0.0", "@metamask/eslint-config-mocha": "^6.0.0",
@ -272,6 +272,7 @@
"gulp-terser-js": "^5.2.2", "gulp-terser-js": "^5.2.2",
"gulp-watch": "^5.0.1", "gulp-watch": "^5.0.1",
"gulp-zip": "^4.0.0", "gulp-zip": "^4.0.0",
"history": "^5.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jsdom": "^11.2.0", "jsdom": "^11.2.0",
"koa": "^2.7.0", "koa": "^2.7.0",
@ -336,7 +337,8 @@
"gc-stats": false, "gc-stats": false,
"github:assemblyscript/assemblyscript": false, "github:assemblyscript/assemblyscript": false,
"tiny-secp256k1": 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 SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/';
const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
export const ALLOWED_SWAPS_CHAIN_IDS = { export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true, [MAINNET_CHAIN_ID]: true,
@ -90,4 +91,5 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = { export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, [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 { strict as assert } from 'assert';
import nock from 'nock'; import nock from 'nock';
import { MILLISECOND, SECOND } from '../constants/time';
import getFetchWithTimeout from './fetch-with-timeout'; import getFetchWithTimeout from './fetch-with-timeout';
describe('getFetchWithTimeout', function () { describe('getFetchWithTimeout', function () {
it('fetches a url', async function () { it('fetches a url', async function () {
nock('https://api.infura.io').get('/money').reply(200, '{"hodl": false}'); nock('https://api.infura.io').get('/money').reply(200, '{"hodl": false}');
const fetchWithTimeout = getFetchWithTimeout(30000); const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
const response = await ( const response = await (
await fetchWithTimeout('https://api.infura.io/money') await fetchWithTimeout('https://api.infura.io/money')
).json(); ).json();
@ -19,10 +20,10 @@ describe('getFetchWithTimeout', function () {
it('throws when the request hits a custom timeout', async function () { it('throws when the request hits a custom timeout', async function () {
nock('https://api.infura.io') nock('https://api.infura.io')
.get('/moon') .get('/moon')
.delay(2000) .delay(SECOND * 2)
.reply(200, '{"moon": "2012-12-21T11:11:11Z"}'); .reply(200, '{"moon": "2012-12-21T11:11:11Z"}');
const fetchWithTimeout = getFetchWithTimeout(123); const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123);
try { try {
await fetchWithTimeout('https://api.infura.io/moon').then((r) => 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 () { it('should abort the request when the custom timeout is hit', async function () {
nock('https://api.infura.io') nock('https://api.infura.io')
.get('/moon') .get('/moon')
.delay(2000) .delay(SECOND * 2)
.reply(200, '{"moon": "2012-12-21T11:11:11Z"}'); .reply(200, '{"moon": "2012-12-21T11:11:11Z"}');
const fetchWithTimeout = getFetchWithTimeout(123); const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123);
try { try {
await fetchWithTimeout('https://api.infura.io/moon').then((r) => await fetchWithTimeout('https://api.infura.io/moon').then((r) =>

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

@ -1,6 +1,7 @@
import { SECOND } from '../constants/time';
import getFetchWithTimeout from './fetch-with-timeout'; 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. * 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) { export function transactionMatchesNetwork(transaction, chainId, networkId) {
if (typeof transaction.chainId !== 'undefined') { if (typeof transaction.chainId !== 'undefined') {
return transaction.chainId === chainId; return transaction.chainId === chainId;
} }
return transaction.metamaskNetworkId === networkId; 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; } = options;
const fixtureServer = new FixtureServer(); const fixtureServer = new FixtureServer();
const ganacheServer = new Ganache(); const ganacheServer = new Ganache();
let secondaryGanacheServer;
let dappServer; let dappServer;
let segmentServer; let segmentServer;
let segmentStub; let segmentStub;
@ -34,6 +35,16 @@ async function withFixtures(options, testSuite) {
let webDriver; let webDriver;
try { try {
await ganacheServer.start(ganacheOptions); 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.start();
await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures)); await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures));
if (dapp) { if (dapp) {
@ -103,6 +114,9 @@ async function withFixtures(options, testSuite) {
} finally { } finally {
await fixtureServer.stop(); await fixtureServer.stop();
await ganacheServer.quit(); await ganacheServer.quit();
if (ganacheOptions?.concurrent) {
await secondaryGanacheServer.quit();
}
if (webDriver) { if (webDriver) {
await webDriver.quit(); 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 () { it(`creates first custom RPC entry`, async function () {
const port = 8546;
const chainId = 1338;
await withFixtures( await withFixtures(
{ {
fixtures: 'imported-account', fixtures: 'imported-account',
ganacheOptions, ganacheOptions: { ...ganacheOptions, concurrent: { port, chainId } },
title: this.test.title, title: this.test.title,
}, },
async ({ driver }) => { async ({ driver }) => {
@ -23,8 +25,8 @@ describe('Stores custom RPC history', function () {
await driver.fill('#password', 'correct horse battery staple'); await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER); await driver.press('#password', driver.Key.ENTER);
const rpcUrl = 'http://127.0.0.1:8545/1'; const rpcUrl = `http://127.0.0.1:${port}`;
const chainId = '0x539'; // Ganache default, decimal 1337 const networkName = 'Secondary Ganache Testnet';
await driver.clickElement('.network-display'); await driver.clickElement('.network-display');
@ -33,17 +35,95 @@ describe('Stores custom RPC history', function () {
await driver.findElement('.settings-page__sub-header-text'); await driver.findElement('.settings-page__sub-header-text');
const customRpcInputs = await driver.findElements('input[type="text"]'); const customRpcInputs = await driver.findElements('input[type="text"]');
const networkNameInput = customRpcInputs[0];
const rpcUrlInput = customRpcInputs[1]; const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2]; const chainIdInput = customRpcInputs[2];
await networkNameInput.clear();
await networkNameInput.sendKeys(networkName);
await rpcUrlInput.clear(); await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(rpcUrl); await rpcUrlInput.sendKeys(rpcUrl);
await chainIdInput.clear(); await chainIdInput.clear();
await chainIdInput.sendKeys(chainId); await chainIdInput.sendKeys(chainId.toString());
await driver.clickElement('.network-form__footer .btn-secondary'); 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 { import {
TRANSACTION_STATUSES, TRANSACTION_STATUSES,
TRANSACTION_TYPES, TRANSACTION_TYPES,
@ -16,7 +17,7 @@ export const txMetaStub = {
type: TRANSACTION_TYPES.SENT_ETHER, type: TRANSACTION_TYPES.SENT_ETHER,
txParams: { txParams: {
from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', from: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
gas: '0x5208', gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x1e8480', gasPrice: '0x1e8480',
to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', to: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
value: '0x0', value: '0x0',
@ -197,7 +198,7 @@ export const txMetaStub = {
type: TRANSACTION_TYPES.SENT_ETHER, type: TRANSACTION_TYPES.SENT_ETHER,
txParams: { txParams: {
from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', from: '0xf231d46dd78806e1dd93442cf33c7671f8538748',
gas: '0x5208', gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x1e8480', gasPrice: '0x1e8480',
nonce: '0x4', nonce: '0x4',
to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', to: '0xf231d46dd78806e1dd93442cf33c7671f8538748',

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

@ -10,7 +10,7 @@ import InfoIcon from '../../ui/icon/info-icon.component';
import Button from '../../ui/button'; import Button from '../../ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent'; 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 { SEND_ROUTE } from '../../../helpers/constants/routes';
import { SEVERITIES } from '../../../helpers/constants/design-system'; import { SEVERITIES } from '../../../helpers/constants/design-system';

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

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

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

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

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

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

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

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

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

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '../../ui/button'; import Button from '../../ui/button';
import LoadingScreen from '../../ui/loading-screen'; import LoadingScreen from '../../ui/loading-screen';
import { SECOND } from '../../../../shared/constants/time';
export default class LoadingNetworkScreen extends PureComponent { export default class LoadingNetworkScreen extends PureComponent {
state = { state = {
@ -27,7 +28,7 @@ export default class LoadingNetworkScreen extends PureComponent {
componentDidMount = () => { componentDidMount = () => {
this.cancelCallTimeout = setTimeout( this.cancelCallTimeout = setTimeout(
this.cancelCall, 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); window.clearTimeout(this.cancelCallTimeout);
this.cancelCallTimeout = setTimeout( this.cancelCallTimeout = setTimeout(
this.cancelCall, 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.setState({ showErrorScreen: false });
this.cancelCallTimeout = setTimeout( this.cancelCallTimeout = setTimeout(
this.cancelCall, 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 PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getAccountLink } from '@metamask/etherscan-link';
import { showModal } from '../../../store/actions'; import { showModal } from '../../../store/actions';
import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; import { CONNECTED_ROUTE } from '../../../helpers/constants/routes';
import { Menu, MenuItem } from '../../ui/menu'; import { Menu, MenuItem } from '../../ui/menu';
import getAccountLink from '../../../helpers/utils/account-link';
import { import {
getCurrentChainId, getCurrentChainId,
getCurrentKeyring, getCurrentKeyring,
@ -14,7 +14,10 @@ import {
getSelectedIdentity, getSelectedIdentity,
} from '../../../selectors'; } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent'; import {
useMetricEvent,
useNewMetricEvent,
} from '../../../hooks/useMetricEvent';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
@ -22,6 +25,14 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); 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({ const openFullscreenEvent = useMetricEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
@ -36,27 +47,25 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
name: 'Viewed Account Details', name: 'Viewed Account Details',
}, },
}); });
const viewOnEtherscanEvent = useMetricEvent({
const openConnectedSitesEvent = useMetricEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
action: 'Account Options', action: 'Account Options',
name: 'Clicked View on Etherscan', name: 'Opened Connected Sites',
}, },
}); });
const openConnectedSitesEvent = useMetricEvent({
eventOpts: { const blockExplorerLinkClickedEvent = useNewMetricEvent({
category: 'Navigation', category: 'Navigation',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Account Options', action: 'Account Options',
name: 'Opened Connected Sites', block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '',
}, },
}); });
const keyring = useSelector(getCurrentKeyring);
const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const selectedIdentity = useSelector(getSelectedIdentity);
const { address } = selectedIdentity;
const isRemovable = keyring.type !== 'HD Key Tree'; const isRemovable = keyring.type !== 'HD Key Tree';
return ( return (
@ -90,9 +99,9 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
viewOnEtherscanEvent(); blockExplorerLinkClickedEvent();
global.platform.openTab({ global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs), url: addressLink,
}); });
onClose(); onClose();
}} }}

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

@ -1,7 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import AccountModalContainer from '../account-modal-container'; import AccountModalContainer from '../account-modal-container';
import getAccountLink from '../../../../helpers/utils/account-link';
import QrView from '../../../ui/qr-code'; import QrView from '../../../ui/qr-code';
import EditableLabel from '../../../ui/editable-label'; import EditableLabel from '../../../ui/editable-label';
import Button from '../../../ui/button'; import Button from '../../../ui/button';
@ -18,6 +19,7 @@ export default class AccountDetailsModal extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
render() { render() {
@ -61,8 +63,20 @@ export default class AccountDetailsModal extends Component {
type="secondary" type="secondary"
className="account-details-modal__button" className="account-details-modal__button"
onClick={() => { 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({ global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs), url: accountLink,
}); });
}} }}
> >

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

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import Modal from '../../modal'; import Modal from '../../modal';
import { addressSummary } from '../../../../helpers/utils/util'; import { addressSummary } from '../../../../helpers/utils/util';
import Identicon from '../../../ui/identicon'; import Identicon from '../../../ui/identicon';
import getAccountLink from '../../../../helpers/utils/account-link';
export default class ConfirmRemoveAccount extends Component { export default class ConfirmRemoveAccount extends Component {
static propTypes = { static propTypes = {
@ -16,6 +16,7 @@ export default class ConfirmRemoveAccount extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
handleRemove = () => { handleRemove = () => {
@ -30,7 +31,7 @@ export default class ConfirmRemoveAccount extends Component {
renderSelectedAccount() { renderSelectedAccount() {
const { t } = this.context; const { t } = this.context;
const { identity } = this.props; const { identity, rpcPrefs, chainId } = this.props;
return ( return (
<div className="confirm-remove-account__account"> <div className="confirm-remove-account__account">
<div className="confirm-remove-account__account__identicon"> <div className="confirm-remove-account__account__identicon">
@ -53,11 +54,27 @@ export default class ConfirmRemoveAccount extends Component {
<div className="confirm-remove-account__account__link"> <div className="confirm-remove-account__account__link">
<a <a
className="" className=""
href={getAccountLink( onClick={() => {
const accountLink = getAccountLink(
identity.address, identity.address,
this.props.chainId, chainId,
this.props.rpcPrefs, 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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={t('etherscanView')} title={t('etherscanView')}

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

@ -4,6 +4,7 @@ import log from 'loglevel';
import { BrowserQRCodeReader } from '@zxing/library'; import { BrowserQRCodeReader } from '@zxing/library';
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { SECOND } from '../../../../../shared/constants/time';
import Spinner from '../../../ui/spinner'; import Spinner from '../../../ui/spinner';
import WebcamUtils from '../../../../helpers/utils/webcam-utils'; import WebcamUtils from '../../../../helpers/utils/webcam-utils';
import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component'; 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(); const { permissions } = await WebcamUtils.checkStatus();
if (permissions) { if (permissions) {
// Let the video stream load first... // Let the video stream load first...
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, SECOND * 2));
if (!this.mounted) { if (!this.mounted) {
return; return;
} }
this.setState({ ready: READY_STATE.READY }); this.setState({ ready: READY_STATE.READY });
} else if (this.mounted) { } else if (this.mounted) {
// Keep checking for permissions // Keep checking for permissions
this.permissionChecker = setTimeout(this.checkPermissions, 1000); this.permissionChecker = setTimeout(this.checkPermissions, SECOND);
} }
} catch (error) { } catch (error) {
if (this.mounted) { 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 Tooltip from '../../ui/tooltip';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import { SECOND } from '../../../../shared/constants/time';
class SelectedAccount extends Component { class SelectedAccount extends Component {
state = { state = {
@ -50,7 +51,7 @@ class SelectedAccount extends Component {
this.setState({ copied: true }); this.setState({ copied: true });
this.copyTimeout = setTimeout( this.copyTimeout = setTimeout(
() => this.setState({ copied: false }), () => this.setState({ copied: false }),
3000, SECOND * 3,
); );
copyToClipboard(checksummedAddress); copyToClipboard(checksummedAddress);
}} }}

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

@ -2,18 +2,19 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { import {
getEthConversionFromWeiHex, getEthConversionFromWeiHex,
getValueFromWeiHex, getValueFromWeiHex,
} from '../../../helpers/utils/conversions.util'; } from '../../../helpers/utils/conversions.util';
import { formatDate } from '../../../helpers/utils/util'; import { formatDate } from '../../../helpers/utils/util';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import TransactionActivityLogIcon from './transaction-activity-log-icon'; import TransactionActivityLogIcon from './transaction-activity-log-icon';
import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants';
export default class TransactionActivityLog extends PureComponent { export default class TransactionActivityLog extends PureComponent {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
static propTypes = { static propTypes = {
@ -31,10 +32,21 @@ export default class TransactionActivityLog extends PureComponent {
}; };
handleActivityClick = (activity) => { handleActivityClick = (activity) => {
const etherscanUrl = getBlockExplorerUrlForTx( const { rpcPrefs } = this.props;
activity, const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs);
this.props.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 }); global.platform.openTab({ url: etherscanUrl });
}; };

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save