1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Bumping package version -> 9.6.0

This commit is contained in:
ryanml 2021-06-10 14:29:29 -07:00
commit f1403f4849
221 changed files with 4776 additions and 3254 deletions

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

24
.storybook/metametrics.js Normal file
View File

@ -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

View File

@ -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,22 +45,36 @@ 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}>
<I18nProvider <Router history={history}>
currentLocale={currentLocale} <MetaMetricsProviderStorybook>
current={current} <I18nProvider
en={allLocales.en} currentLocale={currentLocale}
> current={current}
<LegacyI18nProvider> en={allLocales.en}
<div style={styles}>{story()}</div> >
</LegacyI18nProvider> <LegacyI18nProvider>
</I18nProvider> <div style={styles}>{story()}</div>
</LegacyI18nProvider>
</I18nProvider>
</MetaMetricsProviderStorybook>
</Router>
</Provider> </Provider>
); );
}; };

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,10 @@
"addContact": { "addContact": {
"message": "Add contact" "message": "Add contact"
}, },
"addCustomTokenByContractAddress": {
"message": "Cant 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."
}, },
"swapPriceDifferenceUnavailable": { "swapPriceUnavailableDescription": {
"message": "Market price is unavailable. Make sure you feel comfortable with the returned amount before proceeding." "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."
},
"swapPriceUnavailableTitle": {
"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"
}, },

View File

@ -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"
}, },

View File

@ -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"
}, },

View File

@ -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": "कुलयोग"
}, },

View File

@ -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"
}, },

View File

@ -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"

View File

@ -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": "合計"
}, },

View File

@ -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": "합계"
}, },

View File

@ -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"
}, },

View File

@ -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"
}, },

View File

@ -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": "Итого"
}, },

View File

@ -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"

View File

@ -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"
}, },

View File

@ -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"

View File

@ -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);
}); });
} }

View File

@ -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,
); );
} }

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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();
}); });

View File

@ -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() {

View File

@ -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',

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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,
}, },

View File

@ -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);

View File

@ -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)) {
config[key].subscribe((state) => { if (!config[key]) {
this.updateState({ [key]: state }); throw new Error(`Undefined '${key}'`);
}); }
const store = config[key];
if (store.subscribe) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
} else {
this.controllerMessenger.subscribe(
`${store.name}:stateChange`,
(state) => {
this.updateState({ [key]: state });
},
);
}
} }
} }

View File

@ -1,36 +1,195 @@
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 return flattened state', function () { it('should update structure with BaseController-based controller', function () {
const fooStore = new ObservableStore({ foo: 'foo' }); const controllerMessenger = new ControllerMessenger();
const barStore = new ObservableStore({ bar: 'bar' }); const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore(null, { const store = new ComposableObservableStore({ controllerMessenger });
FooStore: fooStore, store.updateStructure({ OldExample: oldExampleController });
BarStore: barStore, 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,
}); });
assert.deepEqual(store.getFlatState(), { foo: 'foo', bar: 'bar' }); const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ Example: exampleController });
exampleController.updateBar('state');
console.log(exampleController.state);
assert.deepEqual(store.getState(), { Example: { bar: 'state' } });
});
it('should update structure with all three types of stores', function () {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({
Example: exampleController,
OldExample: oldExampleController,
Store: exampleStore,
});
exampleStore.putState('state');
exampleController.updateBar('state');
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), {
Example: { bar: 'state' },
OldExample: { baz: 'state' },
Store: 'state',
});
});
it('should return flattened state', function () {
const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' });
const barController = new ExampleController({
messenger: controllerMessenger,
});
const bazController = new OldExampleController();
const store = new ComposableObservableStore({
config: {
FooStore: fooStore,
BarStore: barController,
BazStore: bazController,
},
controllerMessenger,
state: {
FooStore: fooStore.getState(),
BarStore: barController.state,
BazStore: bazController.state,
},
});
assert.deepEqual(store.getFlatState(), {
foo: 'foo',
bar: 'bar',
baz: 'baz',
});
});
it('should return empty flattened state when not configured', function () {
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({ controllerMessenger });
assert.deepEqual(store.getFlatState(), {});
});
it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: exampleController,
},
}),
);
});
it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({});
assert.throws(() => store.updateStructure({ Example: exampleController }));
});
it('should throw if initialized with undefined config entry', function () {
const controllerMessenger = new ControllerMessenger();
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: undefined,
},
controllerMessenger,
}),
);
}); });
it('should return empty flattened state when not configured', function () { it('should return empty flattened state when not configured', function () {

View File

@ -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'];

View File

@ -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;

View File

@ -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

View File

@ -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,33 +452,37 @@ export default class MetamaskController extends EventEmitter {
NotificationController: this.notificationController, NotificationController: this.notificationController,
}); });
this.memStore = new ComposableObservableStore(null, { this.memStore = new ComposableObservableStore({
AppStateController: this.appStateController.store, config: {
NetworkController: this.networkController.store, AppStateController: this.appStateController.store,
AccountTracker: this.accountTracker.store, NetworkController: this.networkController.store,
TxController: this.txController.memStore, AccountTracker: this.accountTracker.store,
CachedBalancesController: this.cachedBalancesController.store, TxController: this.txController.memStore,
TokenRatesController: this.tokenRatesController.store, CachedBalancesController: this.cachedBalancesController.store,
MessageManager: this.messageManager.memStore, TokenRatesController: this.tokenRatesController.store,
PersonalMessageManager: this.personalMessageManager.memStore, MessageManager: this.messageManager.memStore,
DecryptMessageManager: this.decryptMessageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore, EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
KeyringController: this.keyringController.memStore, TypesMessageManager: this.typedMessageManager.memStore,
PreferencesController: this.preferencesController.store, KeyringController: this.keyringController.memStore,
MetaMetricsController: this.metaMetricsController.store, PreferencesController: this.preferencesController.store,
AddressBookController: this.addressBookController, MetaMetricsController: this.metaMetricsController.store,
CurrencyController: this.currencyRateController, AddressBookController: this.addressBookController,
AlertController: this.alertController.store, CurrencyController: this.currencyRateController,
OnboardingController: this.onboardingController.store, AlertController: this.alertController.store,
IncomingTransactionsController: this.incomingTransactionsController.store, OnboardingController: this.onboardingController.store,
PermissionsController: this.permissionsController.permissions, IncomingTransactionsController: this.incomingTransactionsController
PermissionsMetadata: this.permissionsController.store, .store,
ThreeBoxController: this.threeBoxController.store, PermissionsController: this.permissionsController.permissions,
SwapsController: this.swapsController.store, PermissionsMetadata: this.permissionsController.store,
EnsController: this.ensController.store, ThreeBoxController: this.threeBoxController.store,
ApprovalController: this.approvalController, SwapsController: this.swapsController.store,
NotificationController: this.notificationController, EnsController: this.ensController.store,
ApprovalController: this.approvalController,
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.

View File

@ -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();

View File

@ -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;
}

View File

@ -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,
},
});
});
});

View File

@ -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;

View File

@ -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';

View File

@ -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 };

View File

@ -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) {
return false; if (error.message === `Exited with code '1'`) {
return false;
}
throw error;
} }
} }

View File

@ -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

View File

@ -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,
}, },

View File

@ -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"
]
} }

View File

@ -223,6 +223,11 @@
"js-tokens": true "js-tokens": true
} }
}, },
"@babel/parser": {
"globals": {
"BigInt": true
}
},
"@babel/plugin-proposal-async-generator-functions": { "@babel/plugin-proposal-async-generator-functions": {
"packages": { "packages": {
"@babel/core": true, "@babel/core": true,
@ -869,6 +874,7 @@
}, },
"acorn": { "acorn": {
"globals": { "globals": {
"BigInt": true,
"define": true "define": true
} }
}, },
@ -878,6 +884,9 @@
} }
}, },
"acorn-node": { "acorn-node": {
"globals": {
"BigInt": true
},
"packages": { "packages": {
"acorn": true, "acorn": true,
"acorn-dynamic-import": true, "acorn-dynamic-import": true,
@ -950,6 +959,16 @@
"buffer-equal": true "buffer-equal": true
} }
}, },
"are-we-there-yet": {
"builtin": {
"events.EventEmitter": true,
"util.inherits": true
},
"packages": {
"delegates": true,
"readable-stream": true
}
},
"arr-diff": { "arr-diff": {
"packages": { "packages": {
"arr-flatten": true, "arr-flatten": true,
@ -1302,6 +1321,7 @@
"anymatch": true, "anymatch": true,
"async-each": true, "async-each": true,
"braces": true, "braces": true,
"fsevents": true,
"glob-parent": true, "glob-parent": true,
"inherits": true, "inherits": true,
"is-binary-path": true, "is-binary-path": true,
@ -1553,6 +1573,16 @@
"through2": true "through2": true
} }
}, },
"detect-libc": {
"builtin": {
"child_process.spawnSync": true,
"fs.readdirSync": true,
"os.platform": true
},
"globals": {
"process.env": true
}
},
"detective": { "detective": {
"packages": { "packages": {
"acorn-node": true, "acorn-node": true,
@ -1640,7 +1670,10 @@
"es-abstract": { "es-abstract": {
"globals": { "globals": {
"AggregateError": true, "AggregateError": true,
"Atomics": true,
"BigInt": true,
"FinalizationRegistry": true, "FinalizationRegistry": true,
"SharedArrayBuffer": true,
"WeakRef": true "WeakRef": true
}, },
"packages": { "packages": {
@ -1993,6 +2026,45 @@
"process.version": true "process.version": true
} }
}, },
"fsevents": {
"builtin": {
"events.EventEmitter": true,
"fs.stat": true,
"path.join": true,
"util.inherits": true
},
"globals": {
"__dirname": true,
"process.nextTick": true,
"process.platform": true,
"setImmediate": true
},
"native": true,
"packages": {
"node-pre-gyp": true
}
},
"gauge": {
"builtin": {
"util.format": true
},
"globals": {
"clearInterval": true,
"process": true,
"setImmediate": true,
"setInterval": true
},
"packages": {
"aproba": true,
"console-control-strings": true,
"has-unicode": true,
"object-assign": true,
"signal-exit": true,
"string-width": true,
"strip-ansi": true,
"wide-align": true
}
},
"get-assigned-identifiers": { "get-assigned-identifiers": {
"builtin": { "builtin": {
"assert.equal": true "assert.equal": true
@ -2373,6 +2445,16 @@
"process.argv": true "process.argv": true
} }
}, },
"has-unicode": {
"builtin": {
"os.type": true
},
"globals": {
"process.env.LANG": true,
"process.env.LC_ALL": true,
"process.env.LC_CTYPE": true
}
},
"has-value": { "has-value": {
"packages": { "packages": {
"get-value": true, "get-value": true,
@ -2526,6 +2608,11 @@
"is-plain-object": true "is-plain-object": true
} }
}, },
"is-fullwidth-code-point": {
"packages": {
"number-is-nan": true
}
},
"is-glob": { "is-glob": {
"packages": { "packages": {
"is-extglob": true "is-extglob": true
@ -2910,6 +2997,56 @@
"setTimeout": true "setTimeout": true
} }
}, },
"node-pre-gyp": {
"builtin": {
"events.EventEmitter": true,
"fs.existsSync": true,
"fs.readFileSync": true,
"fs.renameSync": true,
"path.dirname": true,
"path.existsSync": true,
"path.join": true,
"path.resolve": true,
"url.parse": true,
"url.resolve": true,
"util.inherits": true
},
"globals": {
"__dirname": true,
"console.log": true,
"process.arch": true,
"process.cwd": true,
"process.env": true,
"process.platform": true,
"process.version.substr": true,
"process.versions": true
},
"packages": {
"detect-libc": true,
"nopt": true,
"npmlog": true,
"rimraf": true,
"semver": true
}
},
"nopt": {
"builtin": {
"path": true,
"stream.Stream": true,
"url": true
},
"globals": {
"console": true,
"process.argv": true,
"process.env.DEBUG_NOPT": true,
"process.env.NOPT_DEBUG": true,
"process.platform": true
},
"packages": {
"abbrev": true,
"osenv": true
}
},
"normalize-path": { "normalize-path": {
"packages": { "packages": {
"remove-trailing-separator": true "remove-trailing-separator": true
@ -2925,6 +3062,22 @@
"once": true "once": true
} }
}, },
"npmlog": {
"builtin": {
"events.EventEmitter": true,
"util": true
},
"globals": {
"process.nextTick": true,
"process.stderr": true
},
"packages": {
"are-we-there-yet": true,
"console-control-strings": true,
"gauge": true,
"set-blocking": true
}
},
"object-copy": { "object-copy": {
"packages": { "packages": {
"copy-descriptor": true, "copy-descriptor": true,
@ -2937,6 +3090,7 @@
"util.inspect": true "util.inspect": true
}, },
"globals": { "globals": {
"BigInt": true,
"HTMLElement": true "HTMLElement": true
} }
}, },
@ -2991,6 +3145,54 @@
"readable-stream": true "readable-stream": true
} }
}, },
"os-homedir": {
"builtin": {
"os.homedir": true
},
"globals": {
"process.env": true,
"process.getuid": true,
"process.platform": true
}
},
"os-tmpdir": {
"globals": {
"process.env.SystemRoot": true,
"process.env.TEMP": true,
"process.env.TMP": true,
"process.env.TMPDIR": true,
"process.env.windir": true,
"process.platform": true
}
},
"osenv": {
"builtin": {
"child_process.exec": true,
"path": true
},
"globals": {
"process.env.COMPUTERNAME": true,
"process.env.ComSpec": true,
"process.env.EDITOR": true,
"process.env.HOSTNAME": true,
"process.env.PATH": true,
"process.env.PROMPT": true,
"process.env.PS1": true,
"process.env.Path": true,
"process.env.SHELL": true,
"process.env.USER": true,
"process.env.USERDOMAIN": true,
"process.env.USERNAME": true,
"process.env.VISUAL": true,
"process.env.path": true,
"process.nextTick": true,
"process.platform": true
},
"packages": {
"os-homedir": true,
"os-tmpdir": true
}
},
"parent-module": { "parent-module": {
"packages": { "packages": {
"callsites": true "callsites": true
@ -3573,6 +3775,12 @@
"process": true "process": true
} }
}, },
"set-blocking": {
"globals": {
"process.stderr": true,
"process.stdout": true
}
},
"set-value": { "set-value": {
"packages": { "packages": {
"extend-shallow": true, "extend-shallow": true,
@ -3766,6 +3974,7 @@
}, },
"string-width": { "string-width": {
"packages": { "packages": {
"code-point-at": true,
"emoji-regex": true, "emoji-regex": true,
"is-fullwidth-code-point": true, "is-fullwidth-code-point": true,
"strip-ansi": true "strip-ansi": true
@ -4322,6 +4531,11 @@
"isexe": true "isexe": true
} }
}, },
"wide-align": {
"packages": {
"string-width": true
}
},
"write": { "write": {
"builtin": { "builtin": {
"fs.createWriteStream": true, "fs.createWriteStream": true,

View File

@ -4,7 +4,7 @@
"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",
@ -97,7 +97,7 @@
"@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.22.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
} }
} }
} }

11
shared/constants/gas.js Normal file
View File

@ -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)),
};

View File

@ -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,
}; };

5
shared/constants/time.js Normal file
View File

@ -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;

View File

@ -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) =>

View File

@ -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;

View File

@ -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.

View File

@ -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,
);
});
});
});

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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();
} }

View File

@ -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);
});
});
}); });

View File

@ -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',
});
},
);
});
});

View File

@ -12,6 +12,49 @@ 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(
{
fixtures: 'imported-account',
ganacheOptions: { ...ganacheOptions, concurrent: { port, chainId } },
title: this.test.title,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
const rpcUrl = `http://127.0.0.1:${port}`;
const networkName = 'Secondary Ganache Testnet';
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 networkNameInput = customRpcInputs[0];
const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
await networkNameInput.clear();
await networkNameInput.sendKeys(networkName);
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(rpcUrl);
await chainIdInput.clear();
await chainIdInput.sendKeys(chainId.toString());
await driver.clickElement('.network-form__footer .btn-secondary');
await driver.findElement({ text: networkName, tag: 'div' });
},
);
});
it('warns user when they enter url or chainId for an already configured network', async function () {
await withFixtures( await withFixtures(
{ {
fixtures: 'imported-account', fixtures: 'imported-account',
@ -23,8 +66,9 @@ 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'; // duplicate network
const chainId = '0x539'; // Ganache default, decimal 1337 const duplicateRpcUrl = 'http://localhost:8545';
const duplicateChainId = '0x539';
await driver.clickElement('.network-display'); await driver.clickElement('.network-display');
@ -37,13 +81,19 @@ describe('Stores custom RPC history', function () {
const chainIdInput = customRpcInputs[2]; const chainIdInput = customRpcInputs[2];
await rpcUrlInput.clear(); await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(rpcUrl); await rpcUrlInput.sendKeys(duplicateRpcUrl);
await driver.findElement({
text: 'This URL is currently used by the Localhost 8545 network.',
tag: 'p',
});
await chainIdInput.clear(); await chainIdInput.clear();
await chainIdInput.sendKeys(chainId); await chainIdInput.sendKeys(duplicateChainId);
await driver.findElement({
await driver.clickElement('.network-form__footer .btn-secondary'); text:
await driver.findElement({ text: rpcUrl, tag: 'div' }); 'This Chain ID is currently used by the Localhost 8545 network.',
tag: 'p',
});
}, },
); );
}); });

View File

@ -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',

View File

@ -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';

View File

@ -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';

View File

@ -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 }) => {

View File

@ -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">

View File

@ -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()}

View File

@ -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', () => {

View File

@ -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>

View File

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

View File

@ -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
/>, />,
); );

View File

@ -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');

View File

@ -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 {
@ -38,6 +38,7 @@ import {
getSendMaxModeState, getSendMaxModeState,
getAveragePriceEstimateInHexWEI, getAveragePriceEstimateInHexWEI,
isCustomPriceExcessive, isCustomPriceExcessive,
getIsGasEstimatesFetched,
} from '../../../../selectors'; } from '../../../../selectors';
import { import {
@ -55,10 +56,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 +77,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 +86,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,
@ -132,7 +137,7 @@ const mapStateToProps = (state, ownProps) => {
balance, balance,
conversionRate, conversionRate,
}); });
const isGasEstimate = getIsGasEstimatesFetched(state);
return { return {
hideBasic, hideBasic,
isConfirm: isConfirm(state), isConfirm: isConfirm(state),
@ -142,7 +147,10 @@ const mapStateToProps = (state, ownProps) => {
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
customGasTotal, customGasTotal,
newTotalFiat, newTotalFiat,
customPriceIsSafe: isCustomPriceSafe(state), customPriceIsSafe:
(isMainnet || process.env.IN_TEST) && isGasEstimate
? isCustomPriceSafe(state)
: true,
customPriceIsExcessive: isCustomPriceExcessive(state), customPriceIsExcessive: isCustomPriceExcessive(state),
maxModeOn, maxModeOn,
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {

View File

@ -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,
); );
} }
}; };

View File

@ -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,13 +47,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
name: 'Viewed Account Details', name: 'Viewed Account Details',
}, },
}); });
const viewOnEtherscanEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
});
const openConnectedSitesEvent = useMetricEvent({ const openConnectedSitesEvent = useMetricEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
@ -51,12 +56,16 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
}, },
}); });
const keyring = useSelector(getCurrentKeyring); const blockExplorerLinkClickedEvent = useNewMetricEvent({
const chainId = useSelector(getCurrentChainId); category: 'Navigation',
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); event: 'Clicked Block Explorer Link',
const selectedIdentity = useSelector(getSelectedIdentity); properties: {
link_type: 'Account Tracker',
action: 'Account Options',
block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '',
},
});
const { address } = selectedIdentity;
const isRemovable = keyring.type !== 'HD Key Tree'; 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();
}} }}

View File

@ -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');
accountDetailsMenu.prop('onClose')(); await waitFor(() => {
wrapper.update(); accountDetailsMenu.prop('onClose')();
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); wrapper.update();
expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true);
});
}); });
}); });

View File

@ -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,
}); });
}} }}
> >

View File

@ -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,
}, },
}); });
}); });

View File

@ -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={() => {
identity.address, const accountLink = getAccountLink(
this.props.chainId, identity.address,
this.props.rpcPrefs, chainId,
)} rpcPrefs,
);
this.context.trackEvent({
category: 'Accounts',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Remove Account',
block_explorer_domain: accountLink
? new URL(accountLink)?.hostname
: '',
},
});
global.platform.openTab({
url: accountLink,
});
}}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={t('etherscanView')} title={t('etherscanView')}

View File

@ -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();

View File

@ -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) {

View File

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

View File

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

View File

@ -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,
};

View File

@ -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);
}} }}

View File

@ -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()

View File

@ -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 });
}; };

View File

@ -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 {

View File

@ -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',

View File

@ -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',

View File

@ -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';

View File

@ -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,
}; };

View File

@ -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 && (

View File

@ -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',

View File

@ -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]);

View File

@ -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