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

Merge pull request #12478 from MetaMask/Version-v10.4.0

Version v10.4.0 RC
This commit is contained in:
ryanml 2021-11-03 09:47:49 -07:00 committed by GitHub
commit a03a44730c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 3559 additions and 405 deletions

View File

@ -33,6 +33,7 @@ module.exports = {
'nyc_output/**',
'.vscode/**',
'lavamoat/*/policy.json',
'storybook-build/**',
],
extends: [

View File

@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [10.4.0]
### Added
- [#12400](https://github.com/MetaMask/metamask-extension/pull/12400): Add text to Restore Account screen noting current wallet replacement
### Fixed
- [#12420](https://github.com/MetaMask/metamask-extension/pull/12420): Fix missing conversion rates in Swaps token dropdown
- [#12403](https://github.com/MetaMask/metamask-extension/pull/12403): Fix incorrect default locale used during onboarding
- [#12484](https://github.com/MetaMask/metamask-extension/pull/12484): Prevent occasional incorrect "No Quotes Found" result in Swaps
- [#12550](https://github.com/MetaMask/metamask-extension/pull/12550): Prevent occasional 'BigNumber' error on the confirm screen when sending tokens
## [10.3.0]
### Added
- [#12252](https://github.com/MetaMask/metamask-extension/pull/12252): Support type "0" transactions on EIP-1559 networks
@ -2524,7 +2534,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.3.0...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.4.0...HEAD
[10.4.0]: https://github.com/MetaMask/metamask-extension/compare/v10.3.0...v10.4.0
[10.3.0]: https://github.com/MetaMask/metamask-extension/compare/v10.2.2...v10.3.0
[10.2.2]: https://github.com/MetaMask/metamask-extension/compare/v10.2.1...v10.2.2
[10.2.1]: https://github.com/MetaMask/metamask-extension/compare/v10.2.0...v10.2.1

View File

@ -75,19 +75,6 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
[![Architecture Diagram](./docs/architecture.png)][1]
## Development
```bash
yarn
yarn start
```
## Build for Publishing
```bash
yarn dist
```
## Other Docs
- [How to add custom build to Chrome](./docs/add-to-chrome.md)

View File

@ -1357,6 +1357,9 @@
"metametricsCommitmentsAllowOptOut": {
"message": "Always allow you to opt-out via Settings"
},
"metametricsCommitmentsAllowOptOut2": {
"message": "Always be able to opt-out via Settings"
},
"metametricsCommitmentsBoldNever": {
"message": "Never",
"description": "This string is localized separately from some of the commitments so that we can bold it"
@ -1364,6 +1367,9 @@
"metametricsCommitmentsIntro": {
"message": "MetaMask will.."
},
"metametricsCommitmentsNeverCollect": {
"message": "Never collect keys, addresses, transactions, balances, hashes, or any personal information"
},
"metametricsCommitmentsNeverCollectIP": {
"message": "$1 collect your full IP address",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
@ -1372,6 +1378,12 @@
"message": "$1 collect keys, addresses, transactions, balances, hashes, or any personal information",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
},
"metametricsCommitmentsNeverIP": {
"message": "Never collect your full IP address"
},
"metametricsCommitmentsNeverSell": {
"message": "Never sell data for profit. Ever!"
},
"metametricsCommitmentsNeverSellDataForProfit": {
"message": "$1 sell data for profit. Ever!",
"description": "The $1 is the bolded word 'Never', from 'metametricsCommitmentsBoldNever'"
@ -1380,11 +1392,17 @@
"message": "Send anonymized click & pageview events"
},
"metametricsHelpImproveMetaMask": {
"message": "Help Us Improve MetaMask"
"message": "Help us improve MetaMask"
},
"metametricsOptInDescription": {
"message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem."
},
"metametricsOptInDescription2": {
"message": "We would like to gather basic usage data to improve the usability of our product. These metrics will..."
},
"metametricsTitle": {
"message": "Join 6M+ users to improve MetaMask"
},
"mismatchedChain": {
"message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.",
"description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key"
@ -1638,6 +1656,42 @@
"onboardingImportWallet": {
"message": "Import an existing wallet"
},
"onboardingPinExtensionBillboardAccess": {
"message": "Full Access"
},
"onboardingPinExtensionBillboardDescription": {
"message": "These extensions can see and change information"
},
"onboardingPinExtensionBillboardDescription2": {
"message": "on this site."
},
"onboardingPinExtensionBillboardTitle": {
"message": "Extensions"
},
"onboardingPinExtensionChrome": {
"message": "Click the browser extension icon"
},
"onboardingPinExtensionDescription": {
"message": "Pin MetaMask on your browser so it's accessible and easy to view transaction confirmations."
},
"onboardingPinExtensionDescription2": {
"message": "You can open MetaMask by clicking on the extension and access your wallet with 1 click."
},
"onboardingPinExtensionDescription3": {
"message": "Click browser extension icon to access it instantly"
},
"onboardingPinExtensionLabel": {
"message": "Pin MetaMask"
},
"onboardingPinExtensionStep1": {
"message": "1"
},
"onboardingPinExtensionStep2": {
"message": "2"
},
"onboardingPinExtensionTitle": {
"message": "Your MetaMask install is complete!"
},
"onboardingReturnNotice": {
"message": "\"$1\" will close this tab and direct back to $2",
"description": "Return the user to the site that initiated onboarding"
@ -1915,6 +1969,9 @@
"secretPhrase": {
"message": "Only the first account on this wallet will auto load. After completing this process, to add additional accounts, click the drop down menu, then select Create Account."
},
"secretPhraseWarning": {
"message": "If you restore using another Secret Recovery Phrase, your current wallet, accounts and assets will be removed from this app permanently. This action cannot be undone."
},
"secretRecoveryPhrase": {
"message": "Secret Recovery Phrase"
},

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1022 KiB

View File

@ -79,7 +79,7 @@ const initialState = {
routeState: '',
swapsFeatureIsLive: true,
useNewSwapsApi: false,
isFetchingQuotes: false,
saveFetchedQuotes: false,
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
swapsQuotePrefetchingRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
},
@ -209,7 +209,11 @@ export default class SwapsController {
) {
const { chainId } = fetchParamsMetaData;
const {
swapsState: { useNewSwapsApi, quotesPollingLimitEnabled },
swapsState: {
useNewSwapsApi,
quotesPollingLimitEnabled,
saveFetchedQuotes,
},
} = this.store.getState();
if (!fetchParams) {
@ -230,7 +234,9 @@ export default class SwapsController {
const indexOfCurrentCall = this.indexOfNewestCallInFlight + 1;
this.indexOfNewestCallInFlight = indexOfCurrentCall;
this.setIsFetchingQuotes(true);
if (!saveFetchedQuotes) {
this.setSaveFetchedQuotes(true);
}
let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams, {
@ -241,18 +247,17 @@ export default class SwapsController {
]);
const {
swapsState: { isFetchingQuotes },
swapsState: { saveFetchedQuotes: saveFetchedQuotesAfterResponse },
} = this.store.getState();
// If isFetchingQuotes is false, it means a user left Swaps (we cleaned the state)
// If saveFetchedQuotesAfterResponse is false, it means a user left Swaps (we cleaned the state)
// and we don't want to set any API response with quotes into state.
if (!isFetchingQuotes) {
if (!saveFetchedQuotesAfterResponse) {
return [
{}, // quotes
null, // selectedAggId
];
}
this.setIsFetchingQuotes(false);
newQuotes = mapValues(newQuotes, (quote) => ({
...quote,
@ -559,10 +564,10 @@ export default class SwapsController {
this.store.updateState({ swapsState: { ...swapsState, routeState } });
}
setIsFetchingQuotes(status) {
setSaveFetchedQuotes(status) {
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, isFetchingQuotes: status },
swapsState: { ...swapsState, saveFetchedQuotes: status },
});
}

View File

@ -135,7 +135,7 @@ const EMPTY_INIT_STATE = {
swapsQuoteRefreshTime: 60000,
swapsQuotePrefetchingRefreshTime: 60000,
swapsUserFeeLevel: '',
isFetchingQuotes: false,
saveFetchedQuotes: false,
},
};

View File

@ -39,6 +39,7 @@ import {
CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP,
} from '../../../../shared/constants/network';
import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils';
import { readAddressAsContract } from '../../../../shared/modules/contract-utils';
import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils';
import PendingTransactionTracker from './pending-tx-tracker';
@ -648,6 +649,14 @@ export default class TransactionController extends EventEmitter {
newGasParams.gas = customGasSettings?.gas ?? GAS_LIMITS.SIMPLE;
}
if (customGasSettings.estimateSuggested) {
newGasParams.estimateSuggested = customGasSettings.estimateSuggested;
}
if (customGasSettings.estimateUsed) {
newGasParams.estimateUsed = customGasSettings.estimateUsed;
}
if (isEIP1559Transaction(originalTxMeta)) {
previousGasParams.maxFeePerGas = txParams.maxFeePerGas;
previousGasParams.maxPriorityFeePerGas = txParams.maxPriorityFeePerGas;
@ -1234,23 +1243,21 @@ export default class TransactionController extends EventEmitter {
result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
}
let code;
let contractCode;
if (!result) {
try {
code = await this.query.getCode(to);
} catch (e) {
code = null;
log.warn(e);
}
const {
contractCode: resultCode,
isContractAddress,
} = await readAddressAsContract(this.query, to);
const codeIsEmpty = !code || code === '0x' || code === '0x0';
result = codeIsEmpty
? TRANSACTION_TYPES.SIMPLE_SEND
: TRANSACTION_TYPES.CONTRACT_INTERACTION;
contractCode = resultCode;
result = isContractAddress
? TRANSACTION_TYPES.CONTRACT_INTERACTION
: TRANSACTION_TYPES.SIMPLE_SEND;
}
return { type: result, getCodeResponse: code };
return { type: result, getCodeResponse: contractCode };
}
/**
@ -1394,7 +1401,14 @@ export default class TransactionController extends EventEmitter {
status,
chainId,
origin: referrer,
txParams: { gasPrice, gas: gasLimit, maxFeePerGas, maxPriorityFeePerGas },
txParams: {
gasPrice,
gas: gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
estimateSuggested,
estimateUsed,
},
metamaskNetworkId: network,
} = txMeta;
const source = referrer === 'metamask' ? 'user' : 'dapp';
@ -1408,6 +1422,14 @@ export default class TransactionController extends EventEmitter {
gasParams.gas_price = gasPrice;
}
if (estimateSuggested) {
gasParams.estimate_suggested = estimateSuggested;
}
if (estimateUsed) {
gasParams.estimate_used = estimateUsed;
}
const gasParamsInGwei = this._getGasValuesInGWEI(gasParams);
this._trackMetaMetricsEvent({
@ -1442,6 +1464,8 @@ export default class TransactionController extends EventEmitter {
for (const param in gasParams) {
if (isHexString(gasParams[param])) {
gasValuesInGwei[param] = hexWEIToDecGWEI(gasParams[param]);
} else {
gasValuesInGwei[param] = gasParams[param];
}
}
return gasValuesInGwei;

View File

@ -999,6 +999,8 @@ describe('Transaction Controller', function () {
to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
gas: '0x5209',
gasPrice: '0xa',
estimateSuggested: 'medium',
estimateUsed: 'high',
};
txController.txStateManager._addTransactionsToState([
{
@ -1698,6 +1700,8 @@ describe('Transaction Controller', function () {
maxPriorityFeePerGas: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
estimateSuggested: 'medium',
estimateUsed: 'high',
},
type: TRANSACTION_TYPES.SIMPLE_SEND,
origin: 'other',
@ -1724,6 +1728,8 @@ describe('Transaction Controller', function () {
first_seen: 1624408066355,
transaction_envelope_type: 'fee-market',
status: 'unapproved',
estimate_suggested: 'medium',
estimate_used: 'high',
},
};
@ -1791,5 +1797,22 @@ describe('Transaction Controller', function () {
const result = txController._getGasValuesInGWEI(params);
assert.deepEqual(result, expectedParams);
});
it('converts gas values in hex GWEi to dec GWEI, retains estimate fields', function () {
const params = {
max_fee_per_gas: '0x77359400',
max_priority_fee_per_gas: '0x77359400',
estimate_suggested: 'medium',
estimate_used: 'high',
};
const expectedParams = {
max_fee_per_gas: '2',
max_priority_fee_per_gas: '2',
estimate_suggested: 'medium',
estimate_used: 'high',
};
const result = txController._getGasValuesInGWEI(params);
assert.deepEqual(result, expectedParams);
});
});
});

View File

@ -19,6 +19,8 @@ const normalizers = {
maxFeePerGas: addHexPrefix,
maxPriorityFeePerGas: addHexPrefix,
type: addHexPrefix,
estimateSuggested: (estimate) => estimate,
estimateUsed: (estimate) => estimate,
};
export function normalizeAndValidateTxParams(txParams, lowerCase = true) {

View File

@ -323,6 +323,8 @@ describe('txUtils', function () {
gasPrice: '1',
maxFeePerGas: '1',
maxPriorityFeePerGas: '1',
estimateSuggested: 'medium',
estimateUsed: 'high',
type: '1',
};
@ -377,6 +379,17 @@ describe('txUtils', function () {
'0x1',
'type should be hex-prefixed',
);
assert.equal(
normalizedTxParams.estimateSuggested,
'medium',
'estimateSuggested should be the string originally provided',
);
assert.equal(
normalizedTxParams.estimateUsed,
'high',
'estimateSuggested should be the string originally provided',
);
});
});

View File

@ -447,7 +447,7 @@ export default class TransactionStateManager extends EventEmitter {
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusRejected(txId) {
this._setTransactionStatus(txId, 'rejected');
this._setTransactionStatus(txId, TRANSACTION_STATUSES.REJECTED);
this._deleteTransaction(txId);
}

View File

@ -38,9 +38,23 @@ export default async function getFirstPreferredLangCode() {
userPreferredLocaleCodes = [];
}
const firstPreferredLangCode = userPreferredLocaleCodes
let firstPreferredLangCode = userPreferredLocaleCodes
.map((code) => code.toLowerCase().replace('_', '-'))
.find((code) => existingLocaleCodes[code] !== undefined);
.find(
(code) =>
existingLocaleCodes[code] !== undefined ||
existingLocaleCodes[code.split('-')[0]] !== undefined,
);
// if we have matched against a code with a '-' present, meaning its a regional
// code for which we have a non-regioned locale, we need to set firstPreferredLangCode
// to the correct non-regional code.
if (
firstPreferredLangCode !== undefined &&
existingLocaleCodes[firstPreferredLangCode] === undefined
) {
firstPreferredLangCode = firstPreferredLangCode.split('-')[0];
}
return existingLocaleCodes[firstPreferredLangCode] || 'en';
}

View File

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
import { BuildType } from '../../../shared/constants/app';
import extractEthjsErrorMessage from './extractEthjsErrorMessage';
/* eslint-disable prefer-destructuring */
@ -8,6 +9,7 @@ import extractEthjsErrorMessage from './extractEthjsErrorMessage';
const METAMASK_DEBUG = process.env.METAMASK_DEBUG;
const METAMASK_ENVIRONMENT = process.env.METAMASK_ENVIRONMENT;
const SENTRY_DSN_DEV = process.env.SENTRY_DSN_DEV;
const METAMASK_BUILD_TYPE = process.env.METAMASK_BUILD_TYPE;
/* eslint-enable prefer-destructuring */
// This describes the subset of Redux state attached to errors sent to Sentry
@ -87,10 +89,15 @@ export default function setupSentry({ release, getState }) {
sentryTarget = SENTRY_DSN_DEV;
}
const environment =
METAMASK_BUILD_TYPE === BuildType.main
? METAMASK_ENVIRONMENT
: `${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}`;
Sentry.init({
dsn: sentryTarget,
debug: METAMASK_DEBUG,
environment: METAMASK_ENVIRONMENT,
environment,
integrations: [new Dedupe(), new ExtraErrorData()],
release,
beforeSend: (report) => rewriteReport(report),

View File

@ -6,7 +6,7 @@ const pify = require('pify');
const pump = pify(require('pump'));
const { version } = require('../../package.json');
const { createTask, composeParallel } = require('./task');
const { BuildTypes } = require('./utils');
const { BuildType } = require('./utils');
module.exports = createEtcTasks;
@ -38,7 +38,7 @@ function createEtcTasks({ browserPlatforms, buildType, livereload }) {
function createZipTask(platform, buildType) {
return async () => {
const path =
buildType === BuildTypes.main
buildType === BuildType.main
? `metamask-${platform}-${version}`
: `metamask-${buildType}-${platform}-${version}`;
await pump(

View File

@ -16,7 +16,7 @@ const createScriptTasks = require('./scripts');
const createStyleTasks = require('./styles');
const createStaticAssetTasks = require('./static');
const createEtcTasks = require('./etc');
const { BuildTypes, getBrowserVersionMap } = require('./utils');
const { BuildType, getBrowserVersionMap } = require('./utils');
// packages required dynamically via browserify configuration in dependencies
require('loose-envify');
@ -149,7 +149,7 @@ function parseArgv() {
],
string: [NamedArgs.BuildType],
default: {
[NamedArgs.BuildType]: BuildTypes.main,
[NamedArgs.BuildType]: BuildType.main,
[NamedArgs.LintFenceFiles]: true,
[NamedArgs.OmitLockdown]: false,
[NamedArgs.SkipStats]: false,
@ -168,7 +168,7 @@ function parseArgv() {
}
const buildType = argv[NamedArgs.BuildType];
if (!(buildType in BuildTypes)) {
if (!(buildType in BuildType)) {
throw new Error(`MetaMask build: Invalid build type: "${buildType}"`);
}

View File

@ -6,7 +6,7 @@ const baseManifest = require('../../app/manifest/_base.json');
const betaManifestModifications = require('../../app/manifest/_beta_modifications.json');
const { createTask, composeSeries } = require('./task');
const { BuildTypes } = require('./utils');
const { BuildType } = require('./utils');
module.exports = createManifestTasks;
@ -114,7 +114,7 @@ async function writeJson(obj, file) {
function getBuildModifications(buildType) {
const buildModifications = {};
if (buildType === BuildTypes.beta) {
if (buildType === BuildType.beta) {
Object.assign(buildModifications, betaManifestModifications);
}
return buildModifications;

View File

@ -29,10 +29,10 @@ const bifyModuleGroups = require('bify-module-groups');
const metamaskrc = require('rc')('metamask', {
INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID,
INFURA_PROD_PROJECT_ID: process.env.INFURA_PROD_PROJECT_ID,
ONBOARDING_V2: process.env.ONBOARDING_V2,
SEGMENT_HOST: process.env.SEGMENT_HOST,
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY,
SEGMENT_LEGACY_WRITE_KEY: process.env.SEGMENT_LEGACY_WRITE_KEY,
SENTRY_DSN_DEV:
process.env.SENTRY_DSN_DEV ||
'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496',
@ -51,6 +51,51 @@ const {
createRemoveFencedCodeTransform,
} = require('./transforms/remove-fenced-code');
/**
* The build environment. This describes the environment this build was produced in.
*/
const ENVIRONMENT = {
DEVELOPMENT: 'development',
PRODUCTION: 'production',
OTHER: 'other',
PULL_REQUEST: 'pull-request',
RELEASE_CANDIDATE: 'release-candidate',
STAGING: 'staging',
TESTING: 'testing',
};
/**
* Get a value from the configuration, and confirm that it is set.
*
* @param {string} key - The configuration key to retrieve.
* @returns {string} The config entry requested.
* @throws {Error} Throws if the requested key is missing.
*/
function getConfigValue(key) {
const value = metamaskrc[key];
if (!value) {
throw new Error(`Missing config entry for '${key}'`);
}
return value;
}
/**
* Get the appropriate Infura project ID.
*
* @param {object} options - The Infura project ID options.
* @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment.
* @param {boolean} options.testing - Whether the current build is a test build or not.
* @returns {string} The Infura project ID.
*/
function getInfuraProjectId({ environment, testing }) {
if (testing) {
return '00000000000000000000000000000000';
} else if (environment === ENVIRONMENT.PRODUCTION) {
return getConfigValue('INFURA_PROD_PROJECT_ID');
}
return getConfigValue('INFURA_PROJECT_ID');
}
module.exports = createScriptTasks;
function createScriptTasks({
@ -625,7 +670,7 @@ async function bundleIt(buildConfiguration) {
function getEnvironmentVariables({ buildType, devMode, testing }) {
const environment = getEnvironment({ devMode, testing });
if (environment === 'production' && !process.env.SENTRY_DSN) {
if (environment === ENVIRONMENT.PRODUCTION && !process.env.SENTRY_DSN) {
throw new Error('Missing SENTRY_DSN environment variable');
}
return {
@ -633,16 +678,14 @@ function getEnvironmentVariables({ buildType, devMode, testing }) {
METAMASK_ENVIRONMENT: environment,
METAMASK_VERSION: version,
METAMASK_BUILD_TYPE: buildType,
NODE_ENV: devMode ? 'development' : 'production',
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
IN_TEST: testing ? 'true' : false,
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
CONF: devMode ? metamaskrc : {},
SENTRY_DSN: process.env.SENTRY_DSN,
SENTRY_DSN_DEV: metamaskrc.SENTRY_DSN_DEV,
INFURA_PROJECT_ID: testing
? '00000000000000000000000000000000'
: metamaskrc.INFURA_PROJECT_ID,
INFURA_PROJECT_ID: getInfuraProjectId({ environment, testing }),
SEGMENT_HOST: metamaskrc.SEGMENT_HOST,
// When we're in the 'production' environment we will use a specific key only set in CI
// Otherwise we'll use the key from .metamaskrc or from the environment variable. If
@ -650,13 +693,9 @@ function getEnvironmentVariables({ buildType, devMode, testing }) {
// in the build. This is intentional so that developers can contribute to MetaMask without
// inflating event volume.
SEGMENT_WRITE_KEY:
environment === 'production'
environment === ENVIRONMENT.PRODUCTION
? process.env.SEGMENT_PROD_WRITE_KEY
: metamaskrc.SEGMENT_WRITE_KEY,
SEGMENT_LEGACY_WRITE_KEY:
environment === 'production'
? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY
: metamaskrc.SEGMENT_LEGACY_WRITE_KEY,
SWAPS_USE_DEV_APIS: process.env.SWAPS_USE_DEV_APIS === '1',
ONBOARDING_V2: metamaskrc.ONBOARDING_V2 === '1',
};
@ -665,21 +704,21 @@ function getEnvironmentVariables({ buildType, devMode, testing }) {
function getEnvironment({ devMode, testing }) {
// get environment slug
if (devMode) {
return 'development';
return ENVIRONMENT.DEVELOPMENT;
} else if (testing) {
return 'testing';
return ENVIRONMENT.TESTING;
} else if (process.env.CIRCLE_BRANCH === 'master') {
return 'production';
return ENVIRONMENT.PRODUCTION;
} else if (
/^Version-v(\d+)[.](\d+)[.](\d+)/u.test(process.env.CIRCLE_BRANCH)
) {
return 'release-candidate';
return ENVIRONMENT.RELEASE_CANDIDATE;
} else if (process.env.CIRCLE_BRANCH === 'develop') {
return 'staging';
return ENVIRONMENT.STAGING;
} else if (process.env.CIRCLE_PULL_REQUEST) {
return 'pull-request';
return ENVIRONMENT.PULL_REQUEST;
}
return 'other';
return ENVIRONMENT.OTHER;
}
function renderHtmlFile({

View File

@ -6,7 +6,7 @@ const glob = require('fast-glob');
const locales = require('../../app/_locales/index.json');
const { createTask, composeSeries } = require('./task');
const { BuildTypes } = require('./utils');
const { BuildType } = require('./utils');
const EMPTY_JS_FILE = './development/empty.js';
@ -21,7 +21,7 @@ module.exports = function createStaticAssetTasks({
);
const additionalBuildTargets = {
[BuildTypes.beta]: [
[BuildType.beta]: [
{
src: './app/build-types/beta/',
dest: `images`,

View File

@ -1,6 +1,6 @@
const path = require('path');
const { PassThrough, Transform } = require('stream');
const { BuildTypes } = require('../utils');
const { BuildType } = require('../utils');
const { lintTransformedFile } = require('./utils');
const hasOwnProperty = (obj, key) => Reflect.hasOwnProperty.call(obj, key);
@ -86,7 +86,7 @@ function createRemoveFencedCodeTransform(
buildType,
shouldLintTransformedFiles = true,
) {
if (!hasOwnProperty(BuildTypes, buildType)) {
if (!hasOwnProperty(BuildType, buildType)) {
throw new Error(
`Code fencing transform received unrecognized build type "${buildType}".`,
);
@ -136,7 +136,7 @@ const CommandValidators = {
}
params.forEach((param) => {
if (!hasOwnProperty(BuildTypes, param)) {
if (!hasOwnProperty(BuildType, param)) {
throw new Error(
getInvalidParamsMessage(
filePath,

View File

@ -1,5 +1,5 @@
const deepFreeze = require('deep-freeze-strict');
const { BuildTypes } = require('../utils');
const { BuildType } = require('../utils');
const {
createRemoveFencedCodeTransform,
removeFencedCode,
@ -191,7 +191,7 @@ describe('build/transforms/remove-fenced-code', () => {
const mockFileName = 'file.js';
// Valid inputs
Object.keys(BuildTypes).forEach((buildType) => {
Object.keys(BuildType).forEach((buildType) => {
it(`transforms file with fences for build type "${buildType}"`, () => {
expect(
removeFencedCode(
@ -224,7 +224,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode('main'),
),
).toStrictEqual(['', true]);
@ -243,7 +243,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode('main').concat(ignoredLine),
),
).toStrictEqual([ignoredLine, true]);
@ -256,7 +256,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
modifiedInputWithoutFences,
),
).toStrictEqual([modifiedInputWithoutFences, false]);
@ -286,7 +286,7 @@ describe('build/transforms/remove-fenced-code', () => {
inputs.forEach((input) => {
expect(() =>
removeFencedCode(mockFileName, BuildTypes.flask, input),
removeFencedCode(mockFileName, BuildType.flask, input),
).toThrow(
`Empty fence found in file "${mockFileName}":\n${emptyFence}`,
);
@ -313,7 +313,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().replace(
fenceSentinelAndTerminusRegex,
replacement,
@ -373,7 +373,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
@ -419,7 +419,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().replace(directiveString, replacement),
),
).toThrow(
@ -440,7 +440,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().concat(addition),
),
).toThrow(
@ -460,7 +460,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().replace(validTerminus, replacement),
),
).toThrow(
@ -484,7 +484,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode().replace(validCommand, replacement),
),
).toThrow(
@ -513,7 +513,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode(replacement),
),
).toThrow(
@ -526,7 +526,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
getMinimalFencedCode('').replace('()', ''),
),
).toThrow(/No params specified.$/u);
@ -562,7 +562,7 @@ describe('build/transforms/remove-fenced-code', () => {
expect(() =>
removeFencedCode(
mockFileName,
BuildTypes.flask,
BuildType.flask,
input.replace(target, replacement),
),
).toThrow(expectedError);

View File

@ -1,7 +1,12 @@
const semver = require('semver');
const { version } = require('../../package.json');
const BuildTypes = {
/**
* The distribution this build is intended for.
*
* This should be kept in-sync with the `BuildType` map in `shared/constants/app.js`.
*/
const BuildType = {
beta: 'beta',
flask: 'flask',
main: 'main',
@ -35,7 +40,7 @@ function getBrowserVersionMap(platforms) {
[buildType, buildVersion] = prerelease;
if (!String(buildVersion).match(/^\d+$/u)) {
throw new Error(`Invalid prerelease build version: '${buildVersion}'`);
} else if (buildType !== BuildTypes.beta) {
} else if (buildType !== BuildType.beta) {
throw new Error(`Invalid prerelease build type: ${buildType}`);
}
}
@ -58,6 +63,6 @@ function getBrowserVersionMap(platforms) {
}
module.exports = {
BuildTypes,
BuildType,
getBrowserVersionMap,
};

View File

@ -39,7 +39,6 @@ function onError(error) {
*
* SEGMENT_HOST='http://localhost:9090'
* SEGMENT_WRITE_KEY=FAKE
* SEGMENT_LEGACY_WRITE_KEY=FAKE
*
* Note that the Segment keys must also be set - otherwise the extension will not send any
* metric events.

View File

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.3.0",
"version": "10.4.0",
"private": true,
"repository": {
"type": "git",
@ -18,7 +18,7 @@
"benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js",
"benchmark:firefox": "SELENIUM_BROWSER=firefox node test/e2e/benchmark.js",
"build:test": "yarn build test",
"build:test:metrics": "SEGMENT_HOST='http://localhost:9090' SEGMENT_WRITE_KEY='FAKE' SEGMENT_LEGACY_WRITE_KEY='FAKE' yarn build test",
"build:test:metrics": "SEGMENT_HOST='http://localhost:9090' SEGMENT_WRITE_KEY='FAKE' yarn build test",
"test": "yarn lint && yarn test:unit && yarn test:unit:jest",
"dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080",
"dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'",

View File

@ -11,6 +11,17 @@ export const ENVIRONMENT_TYPE_NOTIFICATION = 'notification';
export const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen';
export const ENVIRONMENT_TYPE_BACKGROUND = 'background';
/**
* The distribution this build is intended for.
*
* This should be kept in-sync with the `BuildType` map in `development/build/utils.js`.
*/
export const BuildType = {
beta: 'beta',
flask: 'flask',
main: 'main',
};
export const PLATFORM_BRAVE = 'Brave';
export const PLATFORM_CHROME = 'Chrome';
export const PLATFORM_EDGE = 'Edge';

View File

@ -1,11 +1,11 @@
import { addHexPrefix } from 'ethereumjs-util';
import { MIN_GAS_LIMIT_HEX } from '../../ui/pages/send/send.constants';
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)),
SIMPLE: addHexPrefix(MIN_GAS_LIMIT_HEX),
// a base estimate for token transfers.
BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)),
};

View File

@ -0,0 +1,12 @@
export const readAddressAsContract = async (ethQuery, address) => {
let contractCode;
try {
contractCode = await ethQuery.getCode(address);
} catch (e) {
contractCode = null;
}
const isContractAddress =
contractCode && contractCode !== '0x' && contractCode !== '0x0';
return { contractCode, isContractAddress };
};

View File

@ -0,0 +1,30 @@
const { readAddressAsContract } = require('./contract-utils');
describe('Contract Utils', () => {
it('checks is an address is a contract address or not', async () => {
let mockEthQuery = {
getCode: () => {
return '0xa';
},
};
const { isContractAddress } = await readAddressAsContract(
mockEthQuery,
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84',
);
expect(isContractAddress).toStrictEqual(true);
mockEthQuery = {
getCode: () => {
return '0x';
},
};
const {
isContractAddress: isNotContractAddress,
} = await readAddressAsContract(
mockEthQuery,
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84',
);
expect(isNotContractAddress).toStrictEqual(false);
});
});

View File

@ -1,6 +1,7 @@
const { promises: fs } = require('fs');
const path = require('path');
const Koa = require('koa');
const { isObject, mapValues } = require('lodash');
const CURRENT_STATE_KEY = '__CURRENT__';
const DEFAULT_STATE_KEY = '__DEFAULT__';
@ -8,6 +9,50 @@ const DEFAULT_STATE_KEY = '__DEFAULT__';
const FIXTURE_SERVER_HOST = 'localhost';
const FIXTURE_SERVER_PORT = 12345;
const fixtureSubstitutionPrefix = '__FIXTURE_SUBSTITUTION__';
const fixtureSubstitutionCommands = {
currentDateInMilliseconds: 'currentDateInMilliseconds',
};
/**
* Perform substitutions on a single piece of state.
*
* @param {unknown} partialState - The piece of state to perform substitutions on.
* @returns {unknown} The partial state with substititions performed.
*/
function performSubstitution(partialState) {
if (Array.isArray(partialState)) {
return partialState.map(performSubstitution);
} else if (isObject(partialState)) {
return mapValues(partialState, performSubstitution);
} else if (
typeof partialState === 'string' &&
partialState.startsWith(fixtureSubstitutionPrefix)
) {
const substitutionCommand = partialState.substring(
fixtureSubstitutionPrefix.length,
);
if (
substitutionCommand ===
fixtureSubstitutionCommands.currentDateInMilliseconds
) {
return new Date().getTime();
}
throw new Error(`Unknown substitution command: ${substitutionCommand}`);
}
return partialState;
}
/**
* Substitute values in the state fixture.
*
* @param {object} rawState - The state fixture.
* @returns {object} The state fixture with substitutions performed.
*/
function performStateSubstitutions(rawState) {
return mapValues(rawState, performSubstitution);
}
class FixtureServer {
constructor() {
this._app = new Koa();
@ -57,7 +102,8 @@ class FixtureServer {
state = this._initialStateCache.get(statePath);
} else {
const data = await fs.readFile(statePath);
state = JSON.parse(data.toString('utf-8'));
const rawState = JSON.parse(data.toString('utf-8'));
state = performStateSubstitutions(rawState);
this._initialStateCache.set(statePath, state);
}

View File

@ -12,7 +12,7 @@
"connectedStatusPopoverHasBeenShown": true,
"defaultHomeActiveTabName": null,
"recoveryPhraseReminderHasBeenShown": true,
"recoveryPhraseReminderLastShown": 1627317428214
"recoveryPhraseReminderLastShown": "__FIXTURE_SUBSTITUTION__currentDateInMilliseconds"
},
"CachedBalancesController": {
"cachedBalances": {

View File

@ -24,11 +24,8 @@ export default function AccountListItem({
className="account-list-item__identicon"
diameter={18}
/>
<div className="account-list-item__account-name">{name || address}</div>
{icon && <div className="account-list-item__icon">{icon}</div>}
{icon ? <div className="account-list-item__icon">{icon}</div> : null}
<AccountMismatchWarning address={address} />
</div>

View File

@ -196,7 +196,9 @@ export default class AccountMenu extends Component {
key={identity.address}
>
<div className="account-menu__check-mark">
{isSelected && <div className="account-menu__check-mark-icon" />}
{isSelected ? (
<div className="account-menu__check-mark-icon" />
) : null}
</div>
<Identicon address={identity.address} diameter={24} />
<div className="account-menu__account-info">

View File

@ -93,7 +93,7 @@ export default class ConfirmPageContainerContent extends Component {
return (
<div className="confirm-page-container-content">
{warning && <ConfirmPageContainerWarning warning={warning} />}
{warning ? <ConfirmPageContainerWarning warning={warning} /> : null}
{ethGasPriceWarning && (
<ConfirmPageContainerWarning warning={ethGasPriceWarning} />
)}
@ -124,7 +124,9 @@ export default class ConfirmPageContainerContent extends Component {
submitText={submitText}
disabled={disabled}
>
{unapprovedTxCount > 1 && <a onClick={onCancelAll}>{rejectNText}</a>}
{unapprovedTxCount > 1 ? (
<a onClick={onCancelAll}>{rejectNText}</a>
) : null}
</PageContainerFooter>
</div>
);

View File

@ -56,7 +56,7 @@ export default function ConfirmPageContainerHeader({
</span>
</div>
)}
{!isFullScreen && <NetworkDisplay />}
{isFullScreen ? null : <NetworkDisplay />}
</div>
{children}
</div>

View File

@ -106,9 +106,9 @@ export default class ContactList extends PureComponent {
return (
<div className="send__select-recipient-wrapper__list">
{children || null}
{searchForRecents && this.renderRecents()}
{searchForContacts && this.renderAddressBook()}
{searchForMyAccounts && this.renderMyAccounts()}
{searchForRecents ? this.renderRecents() : null}
{searchForContacts ? this.renderAddressBook() : null}
{searchForMyAccounts ? this.renderMyAccounts() : null}
</div>
);
}

View File

@ -1,7 +1,7 @@
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs';
import { useGasFeeInputs } from '../../../hooks/gasFeeInput/useGasFeeInputs';
import { getGasLoadingAnimationIsShowing } from '../../../ducks/app/app';
import { txParamsAreDappSuggested } from '../../../../shared/modules/transaction.utils';
import { EDIT_GAS_MODES, GAS_LIMITS } from '../../../../shared/constants/gas';
@ -133,20 +133,21 @@ export default function EditGasPopover({
closePopover();
}
const newGasSettings = supportsEIP1559
? {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas ?? gasPrice),
maxPriorityFeePerGas: decGWEIToHexWEI(
maxPriorityFeePerGas ?? maxFeePerGas ?? gasPrice,
),
}
: {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
};
const newGasSettings = {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
estimateSuggested: defaultEstimateToUse,
estimateUsed: estimateToUse,
};
if (supportsEIP1559) {
newGasSettings.maxFeePerGas = decGWEIToHexWEI(maxFeePerGas ?? gasPrice);
newGasSettings.maxPriorityFeePerGas = decGWEIToHexWEI(
maxPriorityFeePerGas ?? maxFeePerGas ?? gasPrice,
);
} else {
newGasSettings.gasPrice = decGWEIToHexWEI(gasPrice);
}
const cleanTransactionParams = { ...updatedTransaction.txParams };
@ -205,6 +206,7 @@ export default function EditGasPopover({
supportsEIP1559,
estimateToUse,
estimatedBaseFee,
defaultEstimateToUse,
]);
let title = t('editGasTitle');

View File

@ -12,7 +12,7 @@ export default class ModalContent extends PureComponent {
return (
<div className="modal-content">
{title && <div className="modal-content__title">{title}</div>}
{title ? <div className="modal-content__title">{title}</div> : null}
{description && (
<div className="modal-content__description">{description}</div>
)}

View File

@ -14,7 +14,7 @@ export default class SignatureRequestHeader extends PureComponent {
return (
<div className="signature-request-header">
<div className="signature-request-header--account">
{fromAccount && <AccountListItem account={fromAccount} />}
{fromAccount ? <AccountListItem account={fromAccount} /> : null}
</div>
<div className="signature-request-header--network">
<NetworkDisplay colored={false} />

View File

@ -3,6 +3,7 @@
display: flex;
justify-content: space-evenly;
width: 500px;
margin: 0 auto;
}
ul.two-steps {

View File

@ -40,7 +40,7 @@ export default class TransactionActivityLogIcon extends PureComponent {
return (
<div className={classnames('transaction-activity-log-icon', className)}>
{imagePath && <img src={imagePath} height="9" width="9" alt="" />}
{imagePath ? <img src={imagePath} height="9" width="9" alt="" /> : null}
</div>
);
}

View File

@ -35,7 +35,7 @@ export default function ActionableMessage({
return (
<div className={actionableMessageClassName}>
{useIcon && <InfoTooltipIcon fillColor={iconFillColor} />}
{useIcon ? <InfoTooltipIcon fillColor={iconFillColor} /> : null}
{infoTooltipText && (
<InfoTooltip
position="left"

View File

@ -66,7 +66,7 @@ const Button = ({
)}
{...buttonProps}
>
{icon && <span className="button__icon">{icon}</span>}
{icon ? <span className="button__icon">{icon}</span> : null}
{children}
</Tag>
);

View File

@ -35,7 +35,7 @@ export default function Chip({
role={isInteractive ? 'button' : undefined}
tabIndex={isInteractive ? 0 : undefined}
>
{leftIcon && <div className="chip__left-icon">{leftIcon}</div>}
{leftIcon ? <div className="chip__left-icon">{leftIcon}</div> : null}
{children ?? (
<Typography
className="chip__label"
@ -47,7 +47,7 @@ export default function Chip({
{label}
</Typography>
)}
{rightIcon && <div className="chip__right-icon">{rightIcon}</div>}
{rightIcon ? <div className="chip__right-icon">{rightIcon}</div> : null}
</div>
);
}

View File

@ -6,7 +6,7 @@ export default function IconWithLabel({ icon, label, className }) {
return (
<div className={classnames('icon-with-label', className)}>
{icon}
{label && <span className="icon-with-label__label">{label}</span>}
{label ? <span className="icon-with-label__label">{label}</span> : null}
</div>
);
}

View File

@ -33,7 +33,7 @@ export default function ListItem({
}
}}
>
{icon && <div className="list-item__icon">{icon}</div>}
{icon ? <div className="list-item__icon">{icon}</div> : null}
<div className="list-item__heading">
{React.isValidElement(title) ? (
title
@ -44,12 +44,16 @@ export default function ListItem({
<div className="list-item__heading-wrap">{titleIcon}</div>
)}
</div>
{subtitle && <div className="list-item__subheading">{subtitle}</div>}
{children && <div className="list-item__actions">{children}</div>}
{midContent && <div className="list-item__mid-content">{midContent}</div>}
{rightContent && (
{subtitle ? (
<div className="list-item__subheading">{subtitle}</div>
) : null}
{children ? <div className="list-item__actions">{children}</div> : null}
{midContent ? (
<div className="list-item__mid-content">{midContent}</div>
) : null}
{rightContent ? (
<div className="list-item__right-content">{rightContent}</div>
)}
) : null}
</div>
);
}

View File

@ -47,7 +47,7 @@ export default function NumericInput({
}
NumericInput.propTypes = {
value: PropTypes.oneOf([PropTypes.number, PropTypes.string]),
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
detailText: PropTypes.string,
onChange: PropTypes.func,
error: PropTypes.string,

View File

@ -21,7 +21,7 @@ export default class PageContainerHeader extends Component {
renderTabs() {
const { tabs } = this.props;
return tabs && <ul className="page-container__tabs">{tabs}</ul>;
return tabs ? <ul className="page-container__tabs">{tabs}</ul> : null;
}
renderCloseAction() {
@ -99,7 +99,9 @@ export default class PageContainerHeader extends Component {
{title}
</div>
)}
{subtitle && <div className="page-container__subtitle">{subtitle}</div>}
{subtitle ? (
<div className="page-container__subtitle">{subtitle}</div>
) : null}
{this.renderCloseAction()}

View File

@ -26,6 +26,10 @@ function QrCodeView(props) {
qrImage.addData(address);
qrImage.make();
const header = message ? (
<div className="qr-code__header">{message}</div>
) : null;
return (
<div className="qr-code">
{Array.isArray(message) ? (
@ -37,9 +41,9 @@ function QrCodeView(props) {
))}
</div>
) : (
message && <div className="qr-code__header">{message}</div>
header
)}
{warning && <span className="qr_code__error">{warning}</span>}
{warning ? <span className="qr_code__error">{warning}</span> : null}
<div
className="qr-code__wrapper"
dangerouslySetInnerHTML={{

View File

@ -108,7 +108,7 @@ export default class UnitInput extends PureComponent {
}}
autoFocus
/>
{suffix && <div className="unit-input__suffix">{suffix}</div>}
{suffix ? <div className="unit-input__suffix">{suffix}</div> : null}
</div>
{children}
</div>

View File

@ -88,6 +88,7 @@ import {
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
// typedefs
/**
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
@ -230,13 +231,12 @@ async function estimateGasLimitForSend({
// address. If this returns 0x, 0x0 or a nullish value then the address
// is an externally owned account (NOT a contract account). For these
// types of transactions the gasLimit will always be 21,000 or 0x5208
const contractCode = Boolean(to) && (await global.eth.getCode(to));
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
const contractCodeIsEmpty =
!contractCode || contractCode === '0x' || contractCode === '0x0';
if (contractCodeIsEmpty && !isNonStandardEthChain) {
const { isContractAddress } = to
? await readAddressAsContract(global.eth, to)
: {};
if (!isContractAddress && !isNonStandardEthChain) {
return GAS_LIMITS.SIMPLE;
} else if (contractCodeIsEmpty && isNonStandardEthChain) {
} else if (!isContractAddress && isNonStandardEthChain) {
isSimpleSendOnNonStandardNetwork = true;
}
}

View File

@ -66,6 +66,7 @@ const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = '/onboarding/secure-your-wallet';
const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings';
const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension';
const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome';
const ONBOARDING_METAMETRICS = '/onboarding/metametrics';
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction';
const CONFIRM_SEND_ETHER_PATH = '/send-ether';
@ -227,4 +228,5 @@ export {
ONBOARDING_UNLOCK_ROUTE,
ONBOARDING_PIN_EXTENSION_ROUTE,
ONBOARDING_WELCOME_ROUTE,
ONBOARDING_METAMETRICS,
};

View File

@ -10,11 +10,6 @@ import {
export default function Authenticated(props) {
const { isUnlocked, completedOnboarding } = props;
switch (true) {
// For ONBOARDING_V2 dev purposes,
// Remove when ONBOARDING_V2 dev complete
case process.env.ONBOARDING_V2 === true:
return <Redirect to={{ pathname: ONBOARDING_ROUTE }} />;
case isUnlocked && completedOnboarding:
return <Route {...props} />;
case !completedOnboarding:

View File

@ -11,6 +11,7 @@ import {
TRANSACTION_ENVELOPE_TYPES,
} from '../../../shared/constants/transaction';
import { addCurrencies } from '../../../shared/modules/conversion.utils';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
import fetchWithCache from './fetch-with-cache';
const hstInterface = new ethers.utils.Interface(abi);
@ -144,10 +145,8 @@ export function getLatestSubmittedTxWithNonce(
}
export async function isSmartContractAddress(address) {
const code = await global.eth.getCode(address);
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
const codeIsEmpty = !code || code === '0x' || code === '0x0';
return !codeIsEmpty;
const { isContractCode } = readAddressAsContract(global.eth, address);
return isContractCode;
}
export function sumHexes(...args) {

View File

@ -0,0 +1,178 @@
import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import {
conversionUtil,
multiplyCurrencies,
} from '../../../shared/modules/conversion.utils';
import {
getConversionRate,
getNativeCurrency,
} from '../../ducks/metamask/metamask';
import {
checkNetworkAndAccountSupports1559,
getCurrentCurrency,
getSelectedAccount,
getShouldShowFiat,
getPreferences,
txDataSelector,
} from '../../selectors';
import { ETH } from '../../helpers/constants/common';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
// Why this number?
// 20 gwei * 21000 gasLimit = 420,000 gwei
// 420,000 gwei is 0.00042 ETH
// 0.00042 ETH * 100000 = $42
export const MOCK_ETH_USD_CONVERSION_RATE = 100000;
export const LEGACY_GAS_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '10',
medium: '20',
high: '30',
},
estimatedGasFeeTimeBounds: {},
};
export const FEE_MARKET_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates: {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100',
},
estimatedBaseFee: '50',
},
estimatedGasFeeTimeBounds: {},
};
export const HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates: {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53000',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70000',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100000',
},
estimatedBaseFee: '50000',
},
estimatedGasFeeTimeBounds: {},
};
export const generateUseSelectorRouter = ({
checkNetworkAndAccountSupports1559Response,
shouldShowFiat = true,
} = {}) => (selector) => {
if (selector === getConversionRate) {
return MOCK_ETH_USD_CONVERSION_RATE;
}
if (selector === getNativeCurrency) {
return ETH;
}
if (selector === getPreferences) {
return {
useNativeCurrencyAsPrimaryCurrency: true,
};
}
if (selector === getCurrentCurrency) {
return 'USD';
}
if (selector === getShouldShowFiat) {
return shouldShowFiat;
}
if (selector === txDataSelector) {
return {
txParams: {
value: '0x5555',
},
};
}
if (selector === getSelectedAccount) {
return {
balance: '0x440aa47cc2556',
};
}
if (selector === checkNetworkAndAccountSupports1559) {
return checkNetworkAndAccountSupports1559Response;
}
return undefined;
};
export function getTotalCostInETH(gwei, gasLimit) {
return multiplyCurrencies(gwei, gasLimit, {
fromDenomination: 'GWEI',
toDenomination: 'ETH',
multiplicandBase: 10,
multiplierBase: 10,
});
}
export function convertFromHexToFiat(value) {
const val = conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
});
return `$${(val * MOCK_ETH_USD_CONVERSION_RATE).toFixed(2)}`;
}
export function convertFromHexToETH(value) {
const val = conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
});
return `${val} ETH`;
}
export const configureEIP1559 = () => {
useGasFeeEstimates.mockImplementation(() => FEE_MARKET_ESTIMATE_RETURN_VALUE);
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
}),
);
};
export const configureLegacy = () => {
useGasFeeEstimates.mockImplementation(() => LEGACY_GAS_ESTIMATE_RETURN_VALUE);
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: false,
}),
);
};
export const configure = () => {
useSelector.mockImplementation(generateUseSelectorRouter());
};

View File

@ -0,0 +1,148 @@
import { useSelector } from 'react-redux';
import {
EDIT_GAS_MODES,
GAS_ESTIMATE_TYPES,
} from '../../../shared/constants/gas';
import {
getMaximumGasTotalInHexWei,
getMinimumGasTotalInHexWei,
} from '../../../shared/modules/gas.utils';
import { PRIMARY, SECONDARY } from '../../helpers/constants/common';
import {
checkNetworkAndAccountSupports1559,
getShouldShowFiat,
} from '../../selectors';
import {
decGWEIToHexWEI,
decimalToHex,
} from '../../helpers/utils/conversions.util';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
/**
* @typedef {Object} GasEstimatesReturnType
* @property {string} [estimatedMinimumFiat] - The amount estimated to be paid
* based on current network conditions. Expressed in user's preferred currency.
* @property {string} [estimatedMaximumFiat] - the maximum amount estimated to be paid if current
* network transaction volume increases. Expressed in user's preferred currency.
* @property {string} [estimatedMaximumNative] - the maximum amount estimated to be paid if the
* current network transaction volume increases. Expressed in the network's native currency.
* @property {string} [estimatedMinimumNative] - the maximum amount estimated to be paid if the
* current network transaction volume increases. Expressed in the network's native currency.
* @property {string} [estimatedMinimumNative] - the maximum amount estimated to be paid if the
* current network transaction volume increases. Expressed in the network's native currency.
* @property {HexWeiString} [estimatedBaseFee] - estimatedBaseFee from fee-market gasFeeEstimates
* in HexWei.
* @property {HexWeiString} [minimumCostInHexWei] - the minimum amount this transaction will cost.
*/
export function useGasEstimates({
editGasMode,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
minimumGasLimit,
transaction,
}) {
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
const {
currency: fiatCurrency,
numberOfDecimals: fiatNumberOfDecimals,
} = useUserPreferencedCurrency(SECONDARY);
const showFiat = useSelector(getShouldShowFiat);
const {
currency: primaryCurrency,
numberOfDecimals: primaryNumberOfDecimals,
} = useUserPreferencedCurrency(PRIMARY);
// We have two helper methods that take an object that can have either
// gasPrice OR the EIP-1559 fields on it, plus gasLimit. This object is
// conditionally set to the appropriate fields to compute the minimum
// and maximum cost of a transaction given the current estimates or selected
// gas fees.
let gasSettings = {
gasLimit: decimalToHex(gasLimit),
};
if (supportsEIP1559) {
gasSettings = {
...gasSettings,
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas || gasPrice || '0'),
maxPriorityFeePerGas: decGWEIToHexWEI(
maxPriorityFeePerGas || maxFeePerGas || gasPrice || '0',
),
baseFeePerGas: decGWEIToHexWEI(gasFeeEstimates.estimatedBaseFee ?? '0'),
};
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.NONE) {
gasSettings = {
...gasSettings,
gasPrice: '0x0',
};
} else {
gasSettings = {
...gasSettings,
gasPrice: decGWEIToHexWEI(gasPrice),
};
}
// The maximum amount this transaction will cost
const maximumCostInHexWei = getMaximumGasTotalInHexWei(gasSettings);
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
gasSettings = { ...gasSettings, gasLimit: decimalToHex(minimumGasLimit) };
}
// The minimum amount this transaction will cost
const minimumCostInHexWei = getMinimumGasTotalInHexWei(gasSettings);
// The estimated total amount of native currency that will be expended
// given the selected gas fees.
const [estimatedMaximumNative] = useCurrencyDisplay(maximumCostInHexWei, {
numberOfDecimals: primaryNumberOfDecimals,
currency: primaryCurrency,
});
const [, { value: estimatedMaximumFiat }] = useCurrencyDisplay(
maximumCostInHexWei,
{
numberOfDecimals: fiatNumberOfDecimals,
currency: fiatCurrency,
},
);
const [estimatedMinimumNative] = useCurrencyDisplay(minimumCostInHexWei, {
numberOfDecimals: primaryNumberOfDecimals,
currency: primaryCurrency,
});
// We also need to display our closest estimate of the low end of estimation
// in fiat.
const [, { value: estimatedMinimumFiat }] = useCurrencyDisplay(
minimumCostInHexWei,
{
numberOfDecimals: fiatNumberOfDecimals,
currency: fiatCurrency,
},
);
return {
estimatedMaximumFiat: showFiat ? estimatedMaximumFiat : '',
estimatedMinimumFiat: showFiat ? estimatedMinimumFiat : '',
estimatedMaximumNative,
estimatedMinimumNative,
estimatedBaseFee: supportsEIP1559
? decGWEIToHexWEI(gasFeeEstimates.estimatedBaseFee ?? '0')
: undefined,
minimumCostInHexWei,
};
}

View File

@ -0,0 +1,192 @@
import { useSelector } from 'react-redux';
import { renderHook } from '@testing-library/react-hooks';
import {
getMaximumGasTotalInHexWei,
getMinimumGasTotalInHexWei,
} from '../../../shared/modules/gas.utils';
import {
decGWEIToHexWEI,
decimalToHex,
} from '../../helpers/utils/conversions.util';
import {
FEE_MARKET_ESTIMATE_RETURN_VALUE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
configureEIP1559,
configureLegacy,
convertFromHexToETH,
convertFromHexToFiat,
generateUseSelectorRouter,
} from './test-utils';
import { useGasEstimates } from './useGasEstimates';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
};
});
const useGasEstimatesHook = (props) =>
useGasEstimates({
transaction: { txParams: { type: '0x2', value: '100' } },
gasLimit: '21000',
gasPrice: '10',
maxPriorityFeePerGas: '10',
maxFeePerGas: '100',
minimumCostInHexWei: '0x5208',
minimumGasLimit: '0x5208',
supportsEIP1559: true,
...FEE_MARKET_ESTIMATE_RETURN_VALUE,
...props,
});
describe('useGasEstimates', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('EIP-1559 scenario', () => {
beforeEach(() => {
configureEIP1559();
});
it('uses new EIP-1559 gas fields to calculate minimum values', () => {
const gasLimit = '21000';
const maxFeePerGas = '100';
const maxPriorityFeePerGas = '10';
const {
estimatedBaseFee,
} = FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates;
const { result } = renderHook(() =>
useGasEstimatesHook({ gasLimit, maxFeePerGas, maxPriorityFeePerGas }),
);
const minimumHexValue = getMinimumGasTotalInHexWei({
baseFeePerGas: decGWEIToHexWEI(estimatedBaseFee),
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
maxPriorityFeePerGas: decGWEIToHexWEI(maxPriorityFeePerGas),
});
expect(result.current.minimumCostInHexWei).toBe(minimumHexValue);
expect(result.current.estimatedMinimumFiat).toBe(
convertFromHexToFiat(minimumHexValue),
);
expect(result.current.estimatedMinimumNative).toBe(
convertFromHexToETH(minimumHexValue),
);
});
it('uses new EIP-1559 gas fields to calculate maximum values', () => {
const gasLimit = '21000';
const maxFeePerGas = '100';
const { result } = renderHook(() =>
useGasEstimatesHook({ gasLimit, maxFeePerGas }),
);
const maximumHexValue = getMaximumGasTotalInHexWei({
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
});
expect(result.current.estimatedMaximumFiat).toBe(
convertFromHexToFiat(maximumHexValue),
);
expect(result.current.estimatedMaximumNative).toBe(
convertFromHexToETH(maximumHexValue),
);
});
it('does not return fiat values if showFiat is false', () => {
const gasLimit = '21000';
const maxFeePerGas = '100';
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
shouldShowFiat: false,
}),
);
const { result } = renderHook(() =>
useGasEstimatesHook({ gasLimit, maxFeePerGas }),
);
expect(result.current.estimatedMaximumFiat).toBe('');
expect(result.current.estimatedMinimumFiat).toBe('');
});
it('uses gasFeeEstimates.estimatedBaseFee prop to calculate estimatedBaseFee', () => {
const {
estimatedBaseFee,
} = FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates;
const { result } = renderHook(() => useGasEstimatesHook());
expect(result.current.estimatedBaseFee).toBe(
decGWEIToHexWEI(estimatedBaseFee),
);
});
});
describe('legacy scenario', () => {
beforeEach(() => {
configureLegacy();
});
it('uses legacy gas fields to calculate minimum values', () => {
const gasLimit = '21000';
const gasPrice = '10';
const { result } = renderHook(() =>
useGasEstimatesHook({
gasLimit,
gasPrice,
supportsEIP1559: false,
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
}),
);
const minimumHexValue = getMinimumGasTotalInHexWei({
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
});
expect(result.current.minimumCostInHexWei).toBe(minimumHexValue);
expect(result.current.estimatedMinimumFiat).toBe(
convertFromHexToFiat(minimumHexValue),
);
expect(result.current.estimatedMinimumNative).toBe(
convertFromHexToETH(minimumHexValue),
);
});
it('uses legacy gas fields to calculate maximum values', () => {
const gasLimit = '21000';
const gasPrice = '10';
const { result } = renderHook(() =>
useGasEstimatesHook({
gasLimit,
gasPrice,
supportsEIP1559: false,
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
}),
);
const maximumHexValue = getMaximumGasTotalInHexWei({
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
});
expect(result.current.estimatedMaximumFiat).toBe(
convertFromHexToFiat(maximumHexValue),
);
expect(result.current.estimatedMaximumNative).toBe(
convertFromHexToETH(maximumHexValue),
);
});
it('estimatedBaseFee is undefined', () => {
const { result } = renderHook(() =>
useGasEstimatesHook({ supportsEIP1559: false }),
);
expect(result.current.estimatedBaseFee).toBeUndefined();
});
});
});

View File

@ -0,0 +1,263 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import {
conversionLessThan,
conversionGreaterThan,
} from '../../../shared/modules/conversion.utils';
import {
checkNetworkAndAccountSupports1559,
getSelectedAccount,
} from '../../selectors';
import { addHexes } from '../../helpers/utils/conversions.util';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import {
bnGreaterThan,
bnLessThan,
bnLessThanEqualTo,
} from '../../helpers/utils/util';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
const HIGH_FEE_WARNING_MULTIPLIER = 1.5;
const validateGasLimit = (gasLimit, minimumGasLimit) => {
const gasLimitTooLow = conversionLessThan(
{ value: gasLimit, fromNumericBase: 'dec' },
{ value: minimumGasLimit || GAS_LIMITS.SIMPLE, fromNumericBase: 'hex' },
);
if (gasLimitTooLow) return GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS;
return undefined;
};
const validateMaxPriorityFee = (maxPriorityFeePerGas, supportsEIP1559) => {
if (!supportsEIP1559) return undefined;
if (bnLessThanEqualTo(maxPriorityFeePerGas, 0)) {
return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM;
}
return undefined;
};
const validateMaxFee = (
maxFeePerGas,
maxPriorityFeeError,
maxPriorityFeePerGas,
supportsEIP1559,
) => {
if (maxPriorityFeeError || !supportsEIP1559) return undefined;
if (bnGreaterThan(maxPriorityFeePerGas, maxFeePerGas)) {
return GAS_FORM_ERRORS.MAX_FEE_IMBALANCE;
}
return undefined;
};
const validateGasPrice = (
isFeeMarketGasEstimate,
gasPrice,
supportsEIP1559,
transaction,
) => {
if (supportsEIP1559 && isFeeMarketGasEstimate) return undefined;
if (
(!supportsEIP1559 || transaction?.txParams?.gasPrice) &&
bnLessThanEqualTo(gasPrice, 0)
) {
return GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW;
}
return undefined;
};
const getMaxPriorityFeeWarning = (
gasFeeEstimates,
isFeeMarketGasEstimate,
isGasEstimatesLoading,
maxPriorityFeePerGas,
supportsEIP1559,
) => {
if (!supportsEIP1559 || !isFeeMarketGasEstimate || isGasEstimatesLoading)
return undefined;
if (
bnLessThan(
maxPriorityFeePerGas,
gasFeeEstimates?.low?.suggestedMaxPriorityFeePerGas,
)
) {
return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW;
}
if (
gasFeeEstimates?.high &&
bnGreaterThan(
maxPriorityFeePerGas,
gasFeeEstimates.high.suggestedMaxPriorityFeePerGas *
HIGH_FEE_WARNING_MULTIPLIER,
)
) {
return GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING;
}
return undefined;
};
const getMaxFeeWarning = (
gasFeeEstimates,
isGasEstimatesLoading,
isFeeMarketGasEstimate,
maxFeeError,
maxPriorityFeeError,
maxFeePerGas,
supportsEIP1559,
) => {
if (
maxPriorityFeeError ||
maxFeeError ||
!isFeeMarketGasEstimate ||
!supportsEIP1559 ||
isGasEstimatesLoading
) {
return undefined;
}
if (bnLessThan(maxFeePerGas, gasFeeEstimates?.low?.suggestedMaxFeePerGas)) {
return GAS_FORM_ERRORS.MAX_FEE_TOO_LOW;
}
if (
gasFeeEstimates?.high &&
bnGreaterThan(
maxFeePerGas,
gasFeeEstimates.high.suggestedMaxFeePerGas * HIGH_FEE_WARNING_MULTIPLIER,
)
) {
return GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING;
}
return undefined;
};
const getBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
const minimumTxCostInHexWei = addHexes(
minimumCostInHexWei,
transaction?.txParams?.value || '0x0',
);
return conversionGreaterThan(
{ value: minimumTxCostInHexWei, fromNumericBase: 'hex' },
{ value: ethBalance, fromNumericBase: 'hex' },
);
};
/**
* @typedef {Object} GasFeeErrorsReturnType
* @property {Object} [gasErrors] - combined map of errors and warnings.
* @property {boolean} [hasGasErrors] - true if there are errors that can block submission.
* @property {Object} gasWarnings - map of gas warnings for EIP-1559 fields.
* @property {boolean} [balanceError] - true if user balance is less than transaction value.
* @property {boolean} [estimatesUnavailableWarning] - true if supportsEIP1559 is true and
* estimate is not of type fee-market.
*/
export function useGasFeeErrors({
gasEstimateType,
gasFeeEstimates,
isGasEstimatesLoading,
gasLimit,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
minimumCostInHexWei,
minimumGasLimit,
transaction,
}) {
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
const isFeeMarketGasEstimate =
gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET;
// Get all errors
const gasLimitError = validateGasLimit(gasLimit, minimumGasLimit);
const maxPriorityFeeError = validateMaxPriorityFee(
maxPriorityFeePerGas,
supportsEIP1559,
);
const maxFeeError = validateMaxFee(
maxFeePerGas,
maxPriorityFeeError,
maxPriorityFeePerGas,
supportsEIP1559,
);
const gasPriceError = validateGasPrice(
isFeeMarketGasEstimate,
gasPrice,
supportsEIP1559,
transaction,
);
// Get all warnings
const maxPriorityFeeWarning = getMaxPriorityFeeWarning(
gasFeeEstimates,
isFeeMarketGasEstimate,
isGasEstimatesLoading,
maxPriorityFeePerGas,
supportsEIP1559,
);
const maxFeeWarning = getMaxFeeWarning(
gasFeeEstimates,
isGasEstimatesLoading,
isFeeMarketGasEstimate,
maxFeeError,
maxPriorityFeeError,
maxFeePerGas,
supportsEIP1559,
);
// Separating errors from warnings so we can know which value problems
// are blocking or simply useful information for the users
const gasErrors = useMemo(() => {
const errors = {};
if (gasLimitError) errors.gasLimit = gasLimitError;
if (maxPriorityFeeError) errors.maxPriorityFee = maxPriorityFeeError;
if (maxFeeError) errors.maxFee = maxFeeError;
if (gasPriceError) errors.gasPrice = gasPriceError;
return errors;
}, [gasLimitError, maxPriorityFeeError, maxFeeError, gasPriceError]);
const gasWarnings = useMemo(() => {
const warnings = {};
if (maxPriorityFeeWarning) warnings.maxPriorityFee = maxPriorityFeeWarning;
if (maxFeeWarning) warnings.maxFee = maxFeeWarning;
return warnings;
}, [maxPriorityFeeWarning, maxFeeWarning]);
const estimatesUnavailableWarning =
supportsEIP1559 && !isFeeMarketGasEstimate;
// Determine if we have any errors which should block submission
const hasGasErrors = Boolean(Object.keys(gasErrors).length);
// Combine the warnings and errors into one object for easier use within the UI.
// This object should have no effect on whether or not the user can submit the form
const errorsAndWarnings = useMemo(
() => ({
...gasWarnings,
...gasErrors,
}),
[gasErrors, gasWarnings],
);
const { balance: ethBalance } = useSelector(getSelectedAccount);
const balanceError = getBalanceError(
minimumCostInHexWei,
transaction,
ethBalance,
);
return {
gasErrors: errorsAndWarnings,
hasGasErrors,
gasWarnings,
balanceError,
estimatesUnavailableWarning,
};
}

View File

@ -0,0 +1,301 @@
import { renderHook } from '@testing-library/react-hooks';
import { useSelector } from 'react-redux';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
import { useGasFeeErrors } from './useGasFeeErrors';
import {
FEE_MARKET_ESTIMATE_RETURN_VALUE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
configureEIP1559,
configureLegacy,
generateUseSelectorRouter,
} from './test-utils';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
};
});
const renderUseGasFeeErrorsHook = (props) => {
return renderHook(() =>
useGasFeeErrors({
transaction: { txParams: { type: '0x2', value: '100' } },
gasLimit: '21000',
gasPrice: '10',
maxPriorityFeePerGas: '10',
maxFeePerGas: '100',
minimumCostInHexWei: '0x5208',
minimumGasLimit: '0x5208',
...FEE_MARKET_ESTIMATE_RETURN_VALUE,
...props,
}),
);
};
describe('useGasFeeErrors', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('gasLimit validation', () => {
beforeEach(() => {
configureLegacy();
});
it('does not returns gasLimitError if gasLimit is not below minimum', () => {
const { result } = renderUseGasFeeErrorsHook(
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
);
expect(result.current.gasErrors.gasLimit).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
it('returns gasLimitError if gasLimit is below minimum', () => {
const { result } = renderUseGasFeeErrorsHook({
gasLimit: '100',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.gasErrors.gasLimit).toBe(
GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS,
);
expect(result.current.hasGasErrors).toBe(true);
});
});
describe('maxPriorityFee validation', () => {
describe('EIP1559 compliant estimates', () => {
beforeEach(() => {
configureEIP1559();
});
it('does not return maxPriorityFeeError if maxPriorityFee is not 0', () => {
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.gasErrors.maxPriorityFee).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
it('return maxPriorityFeeError if maxPriorityFee is 0', () => {
const { result } = renderUseGasFeeErrorsHook({
maxPriorityFeePerGas: '0',
});
expect(result.current.gasErrors.maxPriorityFee).toBe(
GAS_FORM_ERRORS.MAX_PRIORITY_FEE_BELOW_MINIMUM,
);
expect(result.current.hasGasErrors).toBe(true);
});
});
describe('Legacy estimates', () => {
beforeEach(() => {
configureLegacy();
});
it('does not return maxPriorityFeeError if maxPriorityFee is 0', () => {
const { result } = renderUseGasFeeErrorsHook(
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
);
expect(result.current.gasErrors.maxPriorityFee).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
});
});
describe('maxFee validation', () => {
describe('EIP1559 compliant estimates', () => {
beforeEach(() => {
configureEIP1559();
});
it('does not return maxFeeError if maxFee is greater than maxPriorityFee', () => {
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.gasErrors.maxFee).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
it('return maxFeeError if maxFee is less than maxPriorityFee', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '1',
maxPriorityFeePerGas: '10',
});
expect(result.current.gasErrors.maxFee).toBe(
GAS_FORM_ERRORS.MAX_FEE_IMBALANCE,
);
expect(result.current.hasGasErrors).toBe(true);
});
it('does not return MAX_FEE_IMBALANCE error if maxPriorityFeePerGas is 0', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '1',
maxPriorityFeePerGas: '0',
});
expect(result.current.gasErrors.maxFee).toBeUndefined();
});
});
describe('Legacy estimates', () => {
beforeEach(() => {
configureLegacy();
});
it('does not return maxFeeError if maxFee is less than maxPriorityFee', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '1',
maxPriorityFeePerGas: '10',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.gasErrors.maxFee).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
});
});
describe('gasPrice validation', () => {
describe('EIP1559 compliant estimates', () => {
beforeEach(() => {
configureEIP1559();
});
it('does not return gasPriceError if gasPrice is 0', () => {
const { result } = renderUseGasFeeErrorsHook({ gasPrice: '0' });
expect(result.current.gasErrors.gasPrice).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
});
describe('Legacy estimates', () => {
beforeEach(() => {
configureLegacy();
});
it('returns gasPriceError if gasPrice is 0', () => {
const { result } = renderUseGasFeeErrorsHook({
gasPrice: '0',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.gasErrors.gasPrice).toBe(
GAS_FORM_ERRORS.GAS_PRICE_TOO_LOW,
);
expect(result.current.hasGasErrors).toBe(true);
});
it('does not return gasPriceError if gasPrice is > 0', () => {
const { result } = renderUseGasFeeErrorsHook(
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
);
expect(result.current.gasErrors.gasPrice).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
});
});
describe('maxPriorityFee warning', () => {
describe('EIP1559 compliant estimates', () => {
beforeEach(() => {
configureEIP1559();
});
it('does not return maxPriorityFeeWarning if maxPriorityFee is > suggestedMaxPriorityFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.gasWarnings.maxPriorityFee).toBeUndefined();
});
it('return maxPriorityFeeWarning if maxPriorityFee is < suggestedMaxPriorityFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxPriorityFeePerGas: '1',
});
expect(result.current.gasWarnings.maxPriorityFee).toBe(
GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW,
);
});
it('return maxPriorityFeeWarning if maxPriorityFee is > gasFeeEstimates.high.suggestedMaxPriorityFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxPriorityFeePerGas: '100',
});
expect(result.current.gasWarnings.maxPriorityFee).toBe(
GAS_FORM_ERRORS.MAX_PRIORITY_FEE_HIGH_WARNING,
);
});
});
describe('Legacy estimates', () => {
beforeEach(() => {
configureLegacy();
});
it('does not return maxPriorityFeeWarning if maxPriorityFee is < gasFeeEstimates.low.suggestedMaxPriorityFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxPriorityFeePerGas: '1',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.gasWarnings.maxPriorityFee).toBeUndefined();
expect(result.current.hasGasErrors).toBe(false);
});
});
});
describe('maxFee warning', () => {
describe('EIP1559 compliant estimates', () => {
beforeEach(() => {
configureEIP1559();
});
it('does not return maxFeeWarning if maxFee is > suggestedMaxFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.gasWarnings.maxFee).toBeUndefined();
});
it('return maxFeeWarning if maxFee is < suggestedMaxFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '20',
});
expect(result.current.gasWarnings.maxFee).toBe(
GAS_FORM_ERRORS.MAX_FEE_TOO_LOW,
);
});
it('return maxFeeWarning if gasFeeEstimates are high and maxFee is > suggestedMaxFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '1000',
});
expect(result.current.gasWarnings.maxFee).toBe(
GAS_FORM_ERRORS.MAX_FEE_HIGH_WARNING,
);
});
});
describe('Legacy estimates', () => {
beforeEach(() => {
configureLegacy();
});
it('does not return maxFeeWarning if maxFee is < suggestedMaxFeePerGas', () => {
const { result } = renderUseGasFeeErrorsHook({
maxFeePerGas: '1',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.gasWarnings.maxFee).toBeUndefined();
});
});
});
describe('Balance Error', () => {
it('is false if balance is greater than transaction value', () => {
configureEIP1559();
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.balanceError).toBe(false);
});
it('is true if balance is less than transaction value', () => {
configureLegacy();
const { result } = renderUseGasFeeErrorsHook({
transaction: { txParams: { type: '0x2', value: '0x440aa47cc2556' } },
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.balanceError).toBe(true);
});
});
describe('estimatesUnavailableWarning', () => {
it('is false if supportsEIP1559 and gasEstimateType is fee-market', () => {
configureEIP1559();
const { result } = renderUseGasFeeErrorsHook();
expect(result.current.estimatesUnavailableWarning).toBe(false);
});
it('is true if supportsEIP1559 and gasEstimateType is not fee-market', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
}),
);
const { result } = renderUseGasFeeErrorsHook(
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
);
expect(result.current.estimatesUnavailableWarning).toBe(true);
});
});
});

View File

@ -0,0 +1,254 @@
import { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { getAdvancedInlineGasShown } from '../../selectors';
import { hexToDecimal } from '../../helpers/utils/conversions.util';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
import { useGasFeeErrors } from './useGasFeeErrors';
import { useGasPriceInput } from './useGasPriceInput';
import { useMaxFeePerGasInput } from './useMaxFeePerGasInput';
import { useMaxPriorityFeePerGasInput } from './useMaxPriorityFeePerGasInput';
import { useGasEstimates } from './useGasEstimates';
/**
* @typedef {Object} GasFeeInputReturnType
* @property {DecGweiString} [maxFeePerGas] - the maxFeePerGas input value.
* @property {string} [maxFeePerGasFiat] - the maxFeePerGas converted to the
* user's preferred currency.
* @property {(DecGweiString) => void} setMaxFeePerGas - state setter method to
* update the maxFeePerGas.
* @property {DecGweiString} [maxPriorityFeePerGas] - the maxPriorityFeePerGas
* input value.
* @property {string} [maxPriorityFeePerGasFiat] - the maxPriorityFeePerGas
* converted to the user's preferred currency.
* @property {(DecGweiString) => void} setMaxPriorityFeePerGas - state setter
* method to update the maxPriorityFeePerGas.
* @property {DecGweiString} [gasPrice] - the gasPrice input value.
* @property {(DecGweiString) => void} setGasPrice - state setter method to
* update the gasPrice.
* @property {DecGweiString} gasLimit - the gasLimit input value.
* @property {(DecGweiString) => void} setGasLimit - state setter method to
* update the gasLimit.
* @property {EstimateLevel} [estimateToUse] - the estimate level currently
* selected. This will be null if the user has ejected from using the
* estimates.
* @property {([EstimateLevel]) => void} setEstimateToUse - Setter method for
* choosing which EstimateLevel to use.
* @property {string} [estimatedMinimumFiat] - The amount estimated to be paid
* based on current network conditions. Expressed in user's preferred
* currency.
* @property {string} [estimatedMaximumFiat] - the maximum amount estimated to be
* paid if current network transaction volume increases. Expressed in user's
* preferred currency.
* @property {string} [estimatedMaximumNative] - the maximum amount estimated to
* be paid if the current network transaction volume increases. Expressed in
* the network's native currency.
*/
/**
* Uses gasFeeEstimates and state to keep track of user gas fee inputs.
* Will update the gas fee state when estimates update if the user has not yet
* modified the fields.
* @param {EstimateLevel} defaultEstimateToUse - which estimate
* level to default the 'estimateToUse' state variable to.
* @returns {GasFeeInputReturnType & import(
* './useGasFeeEstimates'
* ).GasEstimates} - gas fee input state and the GasFeeEstimates object
*/
export function useGasFeeInputs(
defaultEstimateToUse = 'medium',
transaction,
minimumGasLimit = '0x5208',
editGasMode,
) {
// We need the gas estimates from the GasFeeController in the background.
// Calling this hooks initiates polling for new gas estimates and returns the
// current estimate.
const {
gasEstimateType,
gasFeeEstimates,
isGasEstimatesLoading,
estimatedGasFeeTimeBounds,
} = useGasFeeEstimates();
const userPrefersAdvancedGas = useSelector(getAdvancedInlineGasShown);
const [estimateToUse, setInternalEstimateToUse] = useState(() => {
if (
userPrefersAdvancedGas &&
transaction?.txParams?.maxPriorityFeePerGas &&
transaction?.txParams?.maxFeePerGas
)
return null;
if (transaction) return transaction?.userFeeLevel || null;
return defaultEstimateToUse;
});
const [gasLimit, setGasLimit] = useState(
Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
);
const {
gasPrice,
setGasPrice,
setGasPriceHasBeenManuallySet,
} = useGasPriceInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
transaction,
});
const {
maxFeePerGas,
maxFeePerGasFiat,
setMaxFeePerGas,
} = useMaxFeePerGasInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
transaction,
});
const {
maxPriorityFeePerGas,
maxPriorityFeePerGasFiat,
setMaxPriorityFeePerGas,
} = useMaxPriorityFeePerGasInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
transaction,
});
const {
estimatedBaseFee,
estimatedMaximumFiat,
estimatedMinimumFiat,
estimatedMaximumNative,
estimatedMinimumNative,
minimumCostInHexWei,
} = useGasEstimates({
editGasMode,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
minimumGasLimit,
transaction,
});
const {
balanceError,
estimatesUnavailableWarning,
gasErrors,
gasWarnings,
hasGasErrors,
} = useGasFeeErrors({
gasEstimateType,
gasFeeEstimates,
isGasEstimatesLoading,
gasLimit,
gasPrice,
maxPriorityFeePerGas,
maxFeePerGas,
minimumCostInHexWei,
minimumGasLimit,
transaction,
});
const handleGasLimitOutOfBoundError = useCallback(() => {
if (gasErrors.gasLimit === GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS) {
const transactionGasLimitDec = hexToDecimal(transaction?.txParams?.gas);
const minimumGasLimitDec = hexToDecimal(minimumGasLimit);
setGasLimit(
transactionGasLimitDec > minimumGasLimitDec
? transactionGasLimitDec
: minimumGasLimitDec,
);
}
}, [minimumGasLimit, gasErrors.gasLimit, transaction]);
// When a user selects an estimate level, it will wipe out what they have
// previously put in the inputs. This returns the inputs to the estimated
// values at the level specified.
const setEstimateToUse = useCallback(
(estimateLevel) => {
setInternalEstimateToUse(estimateLevel);
handleGasLimitOutOfBoundError();
setMaxFeePerGas(null);
setMaxPriorityFeePerGas(null);
setGasPrice(null);
setGasPriceHasBeenManuallySet(false);
},
[
setInternalEstimateToUse,
handleGasLimitOutOfBoundError,
setMaxFeePerGas,
setMaxPriorityFeePerGas,
setGasPrice,
setGasPriceHasBeenManuallySet,
],
);
const onManualChange = useCallback(() => {
setInternalEstimateToUse('custom');
handleGasLimitOutOfBoundError();
// Restore existing values
setGasPrice(gasPrice);
setGasLimit(gasLimit);
setMaxFeePerGas(maxFeePerGas);
setMaxPriorityFeePerGas(maxPriorityFeePerGas);
setGasPriceHasBeenManuallySet(true);
}, [
setInternalEstimateToUse,
handleGasLimitOutOfBoundError,
setGasPrice,
gasPrice,
setGasLimit,
gasLimit,
setMaxFeePerGas,
maxFeePerGas,
setMaxPriorityFeePerGas,
maxPriorityFeePerGas,
setGasPriceHasBeenManuallySet,
]);
return {
maxFeePerGas,
maxFeePerGasFiat,
setMaxFeePerGas,
maxPriorityFeePerGas,
maxPriorityFeePerGasFiat,
setMaxPriorityFeePerGas,
gasPrice,
setGasPrice,
gasLimit,
setGasLimit,
estimateToUse,
setEstimateToUse,
estimatedMinimumFiat,
estimatedMaximumFiat,
estimatedMaximumNative,
estimatedMinimumNative,
isGasEstimatesLoading,
gasFeeEstimates,
gasEstimateType,
estimatedGasFeeTimeBounds,
onManualChange,
estimatedBaseFee,
// error and warnings
balanceError,
estimatesUnavailableWarning,
gasErrors,
gasWarnings,
hasGasErrors,
};
}

View File

@ -1,31 +1,29 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import { multiplyCurrencies } from '../../shared/modules/conversion.utils';
import { TRANSACTION_ENVELOPE_TYPES } from '../../shared/constants/transaction';
import {
getConversionRate,
getNativeCurrency,
} from '../ducks/metamask/metamask';
import {
checkNetworkAndAccountSupports1559,
getCurrentCurrency,
getShouldShowFiat,
txDataSelector,
getSelectedAccount,
} from '../selectors';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
import { ETH, PRIMARY } from '../helpers/constants/common';
import { ETH, PRIMARY } from '../../helpers/constants/common';
import { useGasFeeEstimates } from './useGasFeeEstimates';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
import { useGasFeeInputs } from './useGasFeeInputs';
import { useUserPreferencedCurrency } from './useUserPreferencedCurrency';
jest.mock('./useUserPreferencedCurrency', () => ({
import {
MOCK_ETH_USD_CONVERSION_RATE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
FEE_MARKET_ESTIMATE_RETURN_VALUE,
HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE,
configureEIP1559,
configureLegacy,
generateUseSelectorRouter,
getTotalCostInETH,
} from './test-utils';
jest.mock('../useUserPreferencedCurrency', () => ({
useUserPreferencedCurrency: jest.fn(),
}));
jest.mock('./useGasFeeEstimates', () => ({
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
@ -38,116 +36,6 @@ jest.mock('react-redux', () => {
};
});
// Why this number?
// 20 gwei * 21000 gasLimit = 420,000 gwei
// 420,000 gwei is 0.00042 ETH
// 0.00042 ETH * 100000 = $42
const MOCK_ETH_USD_CONVERSION_RATE = 100000;
const LEGACY_GAS_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
low: '10',
medium: '20',
high: '30',
},
estimatedGasFeeTimeBounds: {},
};
const FEE_MARKET_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates: {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100',
},
estimatedBaseFee: '50',
},
estimatedGasFeeTimeBounds: {},
};
const HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates: {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53000',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70000',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100000',
},
estimatedBaseFee: '50000',
},
estimatedGasFeeTimeBounds: {},
};
const generateUseSelectorRouter = ({
checkNetworkAndAccountSupports1559Response,
} = {}) => (selector) => {
if (selector === getConversionRate) {
return MOCK_ETH_USD_CONVERSION_RATE;
}
if (selector === getNativeCurrency) {
return ETH;
}
if (selector === getCurrentCurrency) {
return 'USD';
}
if (selector === getShouldShowFiat) {
return true;
}
if (selector === txDataSelector) {
return {
txParams: {
value: '0x5555',
},
};
}
if (selector === getSelectedAccount) {
return {
balance: '0x440aa47cc2556',
};
}
if (selector === checkNetworkAndAccountSupports1559) {
return checkNetworkAndAccountSupports1559Response;
}
return undefined;
};
function getTotalCostInETH(gwei, gasLimit) {
return multiplyCurrencies(gwei, gasLimit, {
fromDenomination: 'GWEI',
toDenomination: 'ETH',
multiplicandBase: 10,
multiplierBase: 10,
});
}
describe('useGasFeeInputs', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -161,10 +49,7 @@ describe('useGasFeeInputs', () => {
describe('when using gasPrice API for estimation', () => {
beforeEach(() => {
useGasFeeEstimates.mockImplementation(
() => LEGACY_GAS_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(generateUseSelectorRouter());
configureLegacy();
});
it('passes through the raw estimate values from useGasFeeEstimates', () => {
const { result } = renderHook(() => useGasFeeInputs());
@ -226,11 +111,9 @@ describe('useGasFeeInputs', () => {
describe('when transaction is type-0', () => {
beforeEach(() => {
useGasFeeEstimates.mockImplementation(
() => FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(generateUseSelectorRouter());
configureEIP1559();
});
it('returns gasPrice appropriately, and "0" for EIP1559 fields', () => {
const { result } = renderHook(() =>
useGasFeeInputs('medium', {
@ -251,10 +134,7 @@ describe('useGasFeeInputs', () => {
describe('when using EIP 1559 API for estimation', () => {
beforeEach(() => {
useGasFeeEstimates.mockImplementation(
() => FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(generateUseSelectorRouter());
configureEIP1559();
});
it('passes through the raw estimate values from useGasFeeEstimates', () => {
const { result } = renderHook(() => useGasFeeInputs());
@ -326,10 +206,7 @@ describe('useGasFeeInputs', () => {
describe('when balance is sufficient for minimum transaction cost', () => {
beforeEach(() => {
useGasFeeEstimates.mockImplementation(
() => FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(generateUseSelectorRouter());
configureEIP1559();
});
it('should return false', () => {
@ -340,14 +217,10 @@ describe('useGasFeeInputs', () => {
describe('when balance is insufficient for minimum transaction cost', () => {
beforeEach(() => {
configureEIP1559();
useGasFeeEstimates.mockImplementation(
() => HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
}),
);
});
it('should return true', () => {
@ -360,4 +233,79 @@ describe('useGasFeeInputs', () => {
expect(result.current.balanceError).toBe(true);
});
});
describe('callback setEstimateToUse', () => {
beforeEach(() => {
configureEIP1559();
});
it('should change estimateToUse value', () => {
const { result } = renderHook(() =>
useGasFeeInputs(null, {
userFeeLevel: 'medium',
txParams: { gas: '0x5208' },
}),
);
act(() => {
result.current.setEstimateToUse('high');
});
expect(result.current.estimateToUse).toBe('high');
expect(result.current.maxFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.high
.suggestedMaxFeePerGas,
);
expect(result.current.maxPriorityFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.high
.suggestedMaxPriorityFeePerGas,
);
});
});
describe('callback onManualChange', () => {
beforeEach(() => {
configureEIP1559();
});
it('should change estimateToUse value to custom', () => {
const { result } = renderHook(() =>
useGasFeeInputs(null, {
userFeeLevel: 'medium',
txParams: { gas: '0x5208' },
}),
);
act(() => {
result.current.onManualChange();
result.current.setMaxFeePerGas('100');
result.current.setMaxPriorityFeePerGas('10');
});
expect(result.current.estimateToUse).toBe('custom');
expect(result.current.maxFeePerGas).toBe('100');
expect(result.current.maxPriorityFeePerGas).toBe('10');
});
});
describe('when showFiat is false', () => {
beforeEach(() => {
configureEIP1559();
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
shouldShowFiat: false,
}),
);
});
it('does not return fiat values', () => {
const { result } = renderHook(() =>
useGasFeeInputs(null, {
userFeeLevel: 'medium',
txParams: { gas: '0x5208' },
}),
);
expect(result.current.maxFeePerGasFiat).toBe('');
expect(result.current.maxPriorityFeePerGasFiat).toBe('');
expect(result.current.estimatedMaximumFiat).toBe('');
expect(result.current.estimatedMinimumFiat).toBe('');
});
});
});

View File

@ -0,0 +1,62 @@
import { useState } from 'react';
import { isEqual } from 'lodash';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { hexWEIToDecGWEI } from '../../helpers/utils/conversions.util';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { feeParamsAreCustom } from './utils';
function getGasPriceEstimate(gasFeeEstimates, gasEstimateType, estimateToUse) {
if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
return gasFeeEstimates?.[estimateToUse] ?? '0';
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
return gasFeeEstimates?.gasPrice ?? '0';
}
return '0';
}
/**
* @typedef {Object} GasPriceInputsReturnType
* @property {DecGweiString} [gasPrice] - the gasPrice input value.
* @property {(DecGweiString) => void} setGasPrice - state setter method to update the gasPrice.
* @property {(boolean) => true} setGasPriceHasBeenManuallySet - state setter method to update gasPriceHasBeenManuallySet
* field gasPriceHasBeenManuallySet is used in gasPrice calculations.
*/
export function useGasPriceInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
transaction,
}) {
const [gasPriceHasBeenManuallySet, setGasPriceHasBeenManuallySet] = useState(
transaction?.userFeeLevel === 'custom',
);
const [gasPrice, setGasPrice] = useState(() => {
const { gasPrice: txGasPrice } = transaction?.txParams || {};
return txGasPrice && feeParamsAreCustom(transaction)
? Number(hexWEIToDecGWEI(txGasPrice))
: null;
});
const [initialGasPriceEstimates] = useState(gasFeeEstimates);
const gasPriceEstimatesHaveNotChanged = isEqual(
initialGasPriceEstimates,
gasFeeEstimates,
);
const gasPriceToUse =
gasPrice !== null &&
(gasPriceHasBeenManuallySet ||
gasPriceEstimatesHaveNotChanged ||
isLegacyTransaction(transaction?.txParams))
? gasPrice
: getGasPriceEstimate(gasFeeEstimates, gasEstimateType, estimateToUse);
return {
gasPrice: gasPriceToUse,
setGasPrice,
setGasPriceHasBeenManuallySet,
};
}

View File

@ -0,0 +1,96 @@
import { act, renderHook } from '@testing-library/react-hooks';
import {
FEE_MARKET_ESTIMATE_RETURN_VALUE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
configure,
} from './test-utils';
import { useGasPriceInput } from './useGasPriceInput';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
};
});
describe('useGasPriceInput', () => {
beforeEach(() => {
jest.clearAllMocks();
configure();
});
it('returns gasPrice values from transaction if transaction.userFeeLevel is custom', () => {
const { result } = renderHook(() =>
useGasPriceInput({
transaction: {
userFeeLevel: 'custom',
txParams: { gasPrice: '0x5028' },
},
}),
);
expect(result.current.gasPrice).toBe(0.00002052);
});
it('does not return gasPrice values from transaction if transaction.userFeeLevel is not custom', () => {
configure();
const { result } = renderHook(() =>
useGasPriceInput({
estimateToUse: 'high',
transaction: {
userFeeLevel: 'high',
txParams: { gasPrice: '0x5028' },
},
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
}),
);
expect(result.current.gasPrice).toBe('30');
});
it('if no gasPrice is provided returns default estimate for legacy transaction', () => {
const { result } = renderHook(() =>
useGasPriceInput({
estimateToUse: 'medium',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
}),
);
expect(result.current.gasPrice).toBe('20');
});
it('for legacy transaction if estimateToUse is high and no gasPrice is provided returns high estimate value', () => {
const { result } = renderHook(() =>
useGasPriceInput({
estimateToUse: 'high',
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
}),
);
expect(result.current.gasPrice).toBe('30');
});
it('returns 0 if gasPrice is not present in transaction and estimates are also not legacy', () => {
const { result } = renderHook(() =>
useGasPriceInput({
estimateToUse: 'medium',
...FEE_MARKET_ESTIMATE_RETURN_VALUE,
}),
);
expect(result.current.gasPrice).toBe('0');
});
it('returns gasPrice set by user if gasPriceHasBeenManuallySet is true', () => {
const { result } = renderHook(() =>
useGasPriceInput({ estimateToUse: 'medium' }),
);
act(() => {
result.current.setGasPriceHasBeenManuallySet(true);
result.current.setGasPrice(100);
});
expect(result.current.gasPrice).toBe(100);
});
});

View File

@ -0,0 +1,121 @@
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { SECONDARY } from '../../helpers/constants/common';
import { getMaximumGasTotalInHexWei } from '../../../shared/modules/gas.utils';
import {
decGWEIToHexWEI,
decimalToHex,
hexWEIToDecGWEI,
} from '../../helpers/utils/conversions.util';
import {
checkNetworkAndAccountSupports1559,
getShouldShowFiat,
} from '../../selectors';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
import { feeParamsAreCustom, getGasFeeEstimate } from './utils';
const getMaxFeePerGasFromTransaction = (transaction) => {
const { maxFeePerGas, gasPrice } = transaction?.txParams || {};
return Number(hexWEIToDecGWEI(maxFeePerGas || gasPrice));
};
/**
* @typedef {Object} MaxFeePerGasInputReturnType
* @property {(DecGweiString) => void} setMaxFeePerGas - state setter method to
* update the maxFeePerGas.
* @property {string} [maxFeePerGasFiat] - the maxFeePerGas converted to the
* user's preferred currency.
* @property {(DecGweiString) => void} setMaxFeePerGas - state setter
* method to update the setMaxFeePerGas.
*/
export function useMaxFeePerGasInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
transaction,
}) {
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
const {
currency: fiatCurrency,
numberOfDecimals: fiatNumberOfDecimals,
} = useUserPreferencedCurrency(SECONDARY);
const showFiat = useSelector(getShouldShowFiat);
const maxFeePerGasFromTransaction = supportsEIP1559
? getMaxFeePerGasFromTransaction(transaction)
: 0;
// This hook keeps track of a few pieces of transitional state. It is
// transitional because it is only used to modify a transaction in the
// metamask (background) state tree.
const [maxFeePerGas, setMaxFeePerGas] = useState(() => {
if (maxFeePerGasFromTransaction && feeParamsAreCustom(transaction))
return maxFeePerGasFromTransaction;
return null;
});
let gasSettings = {
gasLimit: decimalToHex(gasLimit),
};
if (supportsEIP1559) {
gasSettings = {
...gasSettings,
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas || gasPrice || '0'),
};
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.NONE) {
gasSettings = {
...gasSettings,
gasPrice: '0x0',
};
} else {
gasSettings = {
...gasSettings,
gasPrice: decGWEIToHexWEI(gasPrice),
};
}
const maximumCostInHexWei = getMaximumGasTotalInHexWei(gasSettings);
// We need to display thee estimated fiat currency impact of the maxFeePerGas
// field to the user. This hook calculates that amount. This also works for
// the gasPrice amount because in legacy transactions cost is always gasPrice
// * gasLimit.
const [, { value: maxFeePerGasFiat }] = useCurrencyDisplay(
maximumCostInHexWei,
{
numberOfDecimals: fiatNumberOfDecimals,
currency: fiatCurrency,
},
);
// We specify whether to use the estimate value by checking if the state
// value has been set. The state value is only set by user input and is wiped
// when the user selects an estimate. Default here is '0' to avoid bignumber
// errors in later calculations for nullish values.
const maxFeePerGasToUse =
maxFeePerGas ??
getGasFeeEstimate(
'suggestedMaxFeePerGas',
gasFeeEstimates,
gasEstimateType,
estimateToUse,
maxFeePerGasFromTransaction,
);
return {
maxFeePerGas: maxFeePerGasToUse,
maxFeePerGasFiat: showFiat ? maxFeePerGasFiat : '',
setMaxFeePerGas,
};
}

View File

@ -0,0 +1,129 @@
import { useSelector } from 'react-redux';
import { act, renderHook } from '@testing-library/react-hooks';
import { getMaximumGasTotalInHexWei } from '../../../shared/modules/gas.utils';
import { decimalToHex } from '../../helpers/utils/conversions.util';
import {
FEE_MARKET_ESTIMATE_RETURN_VALUE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
configureEIP1559,
configureLegacy,
convertFromHexToFiat,
generateUseSelectorRouter,
} from './test-utils';
import { useMaxFeePerGasInput } from './useMaxFeePerGasInput';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
};
});
const renderUseMaxFeePerGasInputHook = (props) =>
renderHook(() =>
useMaxFeePerGasInput({
gasLimit: '21000',
estimateToUse: 'medium',
transaction: {
userFeeLevel: 'custom',
txParams: { maxFeePerGas: '0x5028' },
},
...FEE_MARKET_ESTIMATE_RETURN_VALUE,
...props,
}),
);
describe('useMaxFeePerGasInput', () => {
beforeEach(() => {
jest.clearAllMocks();
configureEIP1559();
});
it('returns maxFeePerGas values from transaction if transaction.userFeeLevel is custom', () => {
const { result } = renderUseMaxFeePerGasInputHook();
expect(result.current.maxFeePerGas).toBe(0.00002052);
});
it('returns gasPrice values from transaction if transaction.userFeeLevel is custom and maxFeePerGas is not provided', () => {
const { result } = renderUseMaxFeePerGasInputHook({
transaction: {
userFeeLevel: 'custom',
txParams: { gasPrice: '0x5028' },
},
});
expect(result.current.maxFeePerGas).toBe(0.00002052);
});
it('does not returns maxFeePerGas values from transaction if transaction.userFeeLevel is not custom', () => {
const { result } = renderUseMaxFeePerGasInputHook({
estimateToUse: 'high',
transaction: {
userFeeLevel: 'high',
txParams: { maxFeePerGas: '0x5028' },
},
});
expect(result.current.maxFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.high
.suggestedMaxFeePerGas,
);
});
it('if no maxFeePerGas is provided by user or in transaction it returns value from fee market estimate', () => {
const { result } = renderUseMaxFeePerGasInputHook({
transaction: {
userFeeLevel: 'high',
txParams: {},
},
});
expect(result.current.maxFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium
.suggestedMaxFeePerGas,
);
});
it('maxFeePerGasFiat is maximum amount that the transaction can cost', () => {
const { result } = renderUseMaxFeePerGasInputHook();
const maximumHexValue = getMaximumGasTotalInHexWei({
gasLimit: decimalToHex('21000'),
maxFeePerGas: '0x5028',
});
expect(result.current.maxFeePerGasFiat).toBe(
convertFromHexToFiat(maximumHexValue),
);
});
it('does not return fiat values if showFiat is false', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
shouldShowFiat: false,
}),
);
const { result } = renderUseMaxFeePerGasInputHook();
expect(result.current.maxFeePerGasFiat).toBe('');
});
it('returns 0 if EIP1559 is not supported and legacy gas estimates have been provided', () => {
configureLegacy();
const { result } = renderUseMaxFeePerGasInputHook({
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.maxFeePerGas).toBe('0');
});
it('returns maxFeePerGas set by user if setMaxFeePerGas is called', () => {
const { result } = renderUseMaxFeePerGasInputHook();
act(() => {
result.current.setMaxFeePerGas(100);
});
expect(result.current.maxFeePerGas).toBe(100);
});
});

View File

@ -0,0 +1,97 @@
import { useSelector } from 'react-redux';
import { useState } from 'react';
import { addHexPrefix } from 'ethereumjs-util';
import { SECONDARY } from '../../helpers/constants/common';
import { hexWEIToDecGWEI } from '../../helpers/utils/conversions.util';
import {
checkNetworkAndAccountSupports1559,
getShouldShowFiat,
} from '../../selectors';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
import { feeParamsAreCustom, getGasFeeEstimate } from './utils';
const getMaxPriorityFeePerGasFromTransaction = (transaction) => {
const { maxPriorityFeePerGas, maxFeePerGas, gasPrice } =
transaction?.txParams || {};
return Number(
hexWEIToDecGWEI(maxPriorityFeePerGas || maxFeePerGas || gasPrice),
);
};
/**
* @typedef {Object} MaxPriorityFeePerGasInputReturnType
* @property {DecGweiString} [maxPriorityFeePerGas] - the maxPriorityFeePerGas
* input value.
* @property {string} [maxPriorityFeePerGasFiat] - the maxPriorityFeePerGas
* converted to the user's preferred currency.
* @property {(DecGweiString) => void} setMaxPriorityFeePerGas - state setter
* method to update the maxPriorityFeePerGas.
*/
export function useMaxPriorityFeePerGasInput({
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
transaction,
}) {
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
const {
currency: fiatCurrency,
numberOfDecimals: fiatNumberOfDecimals,
} = useUserPreferencedCurrency(SECONDARY);
const showFiat = useSelector(getShouldShowFiat);
const maxPriorityFeePerGasFromTransaction = supportsEIP1559
? getMaxPriorityFeePerGasFromTransaction(transaction)
: 0;
const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(() => {
if (maxPriorityFeePerGasFromTransaction && feeParamsAreCustom(transaction))
return maxPriorityFeePerGasFromTransaction;
return null;
});
const maxPriorityFeePerGasToUse =
maxPriorityFeePerGas ??
getGasFeeEstimate(
'suggestedMaxPriorityFeePerGas',
gasFeeEstimates,
gasEstimateType,
estimateToUse,
maxPriorityFeePerGasFromTransaction,
);
// We need to display the estimated fiat currency impact of the
// maxPriorityFeePerGas field to the user. This hook calculates that amount.
const [, { value: maxPriorityFeePerGasFiat }] = useCurrencyDisplay(
addHexPrefix(
multiplyCurrencies(maxPriorityFeePerGasToUse, gasLimit, {
toNumericBase: 'hex',
fromDenomination: 'GWEI',
toDenomination: 'WEI',
multiplicandBase: 10,
multiplierBase: 10,
}),
),
{
numberOfDecimals: fiatNumberOfDecimals,
currency: fiatCurrency,
},
);
return {
maxPriorityFeePerGas: maxPriorityFeePerGasToUse,
maxPriorityFeePerGasFiat: showFiat ? maxPriorityFeePerGasFiat : '',
setMaxPriorityFeePerGas,
};
}

View File

@ -0,0 +1,118 @@
import { useSelector } from 'react-redux';
import { act, renderHook } from '@testing-library/react-hooks';
import {
FEE_MARKET_ESTIMATE_RETURN_VALUE,
LEGACY_GAS_ESTIMATE_RETURN_VALUE,
configureEIP1559,
configureLegacy,
convertFromHexToFiat,
generateUseSelectorRouter,
} from './test-utils';
import { useMaxPriorityFeePerGasInput } from './useMaxPriorityFeePerGasInput';
jest.mock('../useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
};
});
const renderUseMaxPriorityFeePerGasInputHook = (props) => {
return renderHook(() =>
useMaxPriorityFeePerGasInput({
gasLimit: '21000',
estimateToUse: 'medium',
transaction: {
userFeeLevel: 'custom',
txParams: { maxPriorityFeePerGas: '0x5028' },
},
...FEE_MARKET_ESTIMATE_RETURN_VALUE,
...props,
}),
);
};
describe('useMaxPriorityFeePerGasInput', () => {
beforeEach(() => {
jest.clearAllMocks();
configureEIP1559();
});
it('returns maxPriorityFeePerGas values from transaction if transaction.userFeeLevel is custom', () => {
const { result } = renderUseMaxPriorityFeePerGasInputHook();
expect(result.current.maxPriorityFeePerGas).toBe(0.00002052);
expect(result.current.maxPriorityFeePerGasFiat).toBe(
convertFromHexToFiat('0x5028'),
);
});
it('returns maxFeePerGas values from transaction if transaction.userFeeLevel is custom and maxPriorityFeePerGas is not provided', () => {
const { result } = renderUseMaxPriorityFeePerGasInputHook({
transaction: {
userFeeLevel: 'custom',
txParams: { maxFeePerGas: '0x5028' },
},
});
expect(result.current.maxPriorityFeePerGas).toBe(0.00002052);
});
it('does not returns maxPriorityFeePerGas values from transaction if transaction.userFeeLevel is not custom', () => {
const { result } = renderUseMaxPriorityFeePerGasInputHook({
estimateToUse: 'high',
transaction: {
userFeeLevel: 'high',
txParams: { maxPriorityFeePerGas: '0x5028' },
},
});
expect(result.current.maxPriorityFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.high
.suggestedMaxPriorityFeePerGas,
);
});
it('if no maxPriorityFeePerGas is provided by user or in transaction it returns value from fee market estimate', () => {
const { result } = renderUseMaxPriorityFeePerGasInputHook({
transaction: {
txParams: {},
},
});
expect(result.current.maxPriorityFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium
.suggestedMaxPriorityFeePerGas,
);
});
it('does not return fiat values if showFiat is false', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({
checkNetworkAndAccountSupports1559Response: true,
shouldShowFiat: false,
}),
);
const { result } = renderUseMaxPriorityFeePerGasInputHook();
expect(result.current.maxPriorityFeePerGasFiat).toBe('');
});
it('returns 0 if EIP1559 is not supported and gas estimates are legacy', () => {
configureLegacy();
const { result } = renderUseMaxPriorityFeePerGasInputHook({
...LEGACY_GAS_ESTIMATE_RETURN_VALUE,
});
expect(result.current.maxPriorityFeePerGas).toBe('0');
});
it('returns maxPriorityFeePerGas set by user if setMaxPriorityFeePerGas is called', () => {
const { result } = renderUseMaxPriorityFeePerGasInputHook();
act(() => {
result.current.setMaxPriorityFeePerGas(100);
});
expect(result.current.maxPriorityFeePerGas).toBe(100);
});
});

View File

@ -0,0 +1,17 @@
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
export function getGasFeeEstimate(
field,
gasFeeEstimates,
gasEstimateType,
estimateToUse,
fallback = '0',
) {
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
return gasFeeEstimates?.[estimateToUse]?.[field] ?? String(fallback);
}
return String(fallback);
}
export const feeParamsAreCustom = (transaction) =>
!transaction?.userFeeLevel || transaction?.userFeeLevel === 'custom';

View File

@ -16,6 +16,7 @@ import { getConversionRate } from '../ducks/metamask/metamask';
import { getSwapsTokens } from '../ducks/swaps/swaps';
import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { useEqualityCheck } from './useEqualityCheck';
const shuffledContractMap = shuffle(
@ -45,7 +46,7 @@ export function getRenderableTokenData(
getTokenFiatAmount(
isSwapsDefaultTokenSymbol(symbol, chainId)
? 1
: contractExchangeRates[address],
: contractExchangeRates[toChecksumHexAddress(address)],
conversionRate,
currentCurrency,
string,
@ -56,7 +57,7 @@ export function getRenderableTokenData(
getTokenFiatAmount(
isSwapsDefaultTokenSymbol(symbol, chainId)
? 1
: contractExchangeRates[address],
: contractExchangeRates[toChecksumHexAddress(address)],
conversionRate,
currentCurrency,
string,
@ -139,7 +140,7 @@ export function useTokensToSearch({
};
const memoizedSwapsAndUserTokensWithoutDuplicities = uniqBy(
[...memoizedTokensToSearch, ...memoizedUsersToken],
[memoizedDefaultToken, ...memoizedTokensToSearch, ...memoizedUsersToken],
(token) => token.address.toLowerCase(),
);
@ -185,6 +186,7 @@ export function useTokensToSearch({
conversionRate,
currentCurrency,
memoizedTopTokens,
memoizedDefaultToken,
chainId,
tokenList,
useTokenDetection,

View File

@ -35,7 +35,9 @@ export default function ConfirmTokenTransactionBase({
}
const decimalEthValue = new BigNumber(tokenAmount)
.times(new BigNumber(contractExchangeRate))
.times(
new BigNumber(contractExchangeRate ? String(contractExchangeRate) : 0),
)
.toFixed();
return getWeiHexFromDecimalValue({

View File

@ -51,6 +51,7 @@ import {
} from '../../store/actions';
import Typography from '../../components/ui/typography/typography';
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
const renderHeartBeatIfNotInTest = () =>
process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />;
@ -232,7 +233,7 @@ export default class ConfirmTransactionBase extends Component {
};
}
if (hexToDecimal(customGas.gasLimit) < 21000) {
if (hexToDecimal(customGas.gasLimit) < Number(MIN_GAS_LIMIT_DEC)) {
return {
valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,

View File

@ -273,7 +273,7 @@ class AccountList extends Component {
{this.renderPagination()}
{this.renderButtons()}
{this.renderForgetDevice()}
{showPopover && this.renderSelectPathPopover()}
{showPopover ? this.renderSelectPathPopover() : null}
</div>
);
}

View File

@ -235,7 +235,7 @@ export default class SelectHardware extends Component {
<div className="new-external-account-form">
{this.renderHeader()}
{this.renderButtons()}
{this.state.selectedDevice && this.renderTutorialsteps()}
{this.state.selectedDevice ? this.renderTutorialsteps() : null}
{this.renderContinueButton()}
</div>
);

View File

@ -258,7 +258,9 @@ export default class ImportWithSeedPhrase extends PureComponent {
autoComplete="off"
/>
)}
{seedPhraseError && <span className="error">{seedPhraseError}</span>}
{seedPhraseError ? (
<span className="error">{seedPhraseError}</span>
) : null}
<div
className="first-time-flow__checkbox-container"
onClick={this.toggleShowSeedPhrase}

View File

@ -113,10 +113,18 @@
color: #1b344d;
}
&__selector-typography {
line-height: 22px;
display: flex;
align-items: center;
color: #000;
margin-top: 18px;
}
&__input-wrapper {
display: flex;
flex-flow: column nowrap;
margin-top: 30px;
margin-top: 18px;
}
&__input {

View File

@ -163,6 +163,9 @@ class RestoreVaultPage extends Component {
<div className="import-account__selector-label">
{this.context.t('secretPhrase')}
</div>
<div className="import-account__selector-typography">
{this.context.t('secretPhraseWarning')}
</div>
<div className="import-account__input-wrapper">
<label className="import-account__input-label">
{this.context.t('walletSeedRestore')}

View File

@ -6,6 +6,8 @@
@import 'creation-successful/index';
@import 'welcome/index';
@import 'import-srp/index';
@import 'pin-extension/index';
@import 'metametrics/index';
.onboarding-flow {
width: 100%;

View File

@ -0,0 +1,41 @@
.onboarding-metametrics {
width: 600px;
ul {
margin: 24px 0 0 0;
li {
padding-bottom: 20px;
display: flex;
}
}
.fa {
width: 16px;
}
.fa-check {
margin-inline-end: 12px;
color: #1acc56;
}
.fa-times {
margin-inline-end: 12px;
color: #d0021b;
}
&__terms a {
color: $Blue-500;
}
&__buttons {
margin: 24px auto 0 auto;
justify-content: space-between;
display: flex;
button {
margin-bottom: 24px;
width: 200px;
}
}
}

View File

@ -0,0 +1,152 @@
import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import Typography from '../../../components/ui/typography/typography';
import {
TYPOGRAPHY,
FONT_WEIGHT,
TEXT_ALIGN,
COLORS,
} from '../../../helpers/constants/design-system';
import Button from '../../../components/ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { setParticipateInMetaMetrics } from '../../../store/actions';
import {
getFirstTimeFlowTypeRoute,
getFirstTimeFlowType,
getParticipateInMetaMetrics,
} from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
const firstTimeFlowTypeNameMap = {
create: 'Selected Create New Wallet',
import: 'Selected Import Wallet',
};
export default function OnboardingMetametrics() {
const t = useI18nContext();
const dispatch = useDispatch();
const history = useHistory();
const nextRoute = useSelector(getFirstTimeFlowTypeRoute);
const firstTimeFlowType = useSelector(getFirstTimeFlowType);
const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics);
const firstTimeSelectionMetaMetricsName =
firstTimeFlowTypeNameMap[firstTimeFlowType];
const metricsEvent = useContext(MetaMetricsContext);
const onConfirm = async () => {
const [, metaMetricsId] = await dispatch(setParticipateInMetaMetrics(true));
try {
if (!participateInMetaMetrics) {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt In',
},
isOptIn: true,
flushImmediately: true,
});
}
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import or Create',
name: firstTimeSelectionMetaMetricsName,
},
isOptIn: true,
metaMetricsId,
flushImmediately: true,
});
} finally {
history.push(nextRoute);
}
};
const onCancel = async () => {
await dispatch(setParticipateInMetaMetrics(false));
try {
if (!participateInMetaMetrics) {
metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Metrics Option',
name: 'Metrics Opt Out',
},
isOptIn: true,
flushImmediately: true,
});
}
} finally {
history.push(nextRoute);
}
};
return (
<div className="onboarding-metametrics">
<Typography
variant={TYPOGRAPHY.H2}
align={TEXT_ALIGN.CENTER}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('metametricsTitle')}
</Typography>
<Typography align={TEXT_ALIGN.CENTER}>
{t('metametricsOptInDescription2')}
</Typography>
<ul>
<li>
<i className="fa fa-check" />
{t('metametricsCommitmentsAllowOptOut2')}
</li>
<li>
<i className="fa fa-check" />
{t('metametricsCommitmentsSendAnonymizedEvents')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverCollect')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverIP')}
</li>
<li>
<i className="fa fa-times" />
{t('metametricsCommitmentsNeverSell')}
</li>
</ul>
<Typography
color={COLORS.UI4}
align={TEXT_ALIGN.CENTER}
variant={TYPOGRAPHY.H6}
className="onboarding-metametrics__terms"
>
{t('gdprMessage', [
<a
key="metametrics-bottom-text-wrapper"
href="https://metamask.io/privacy.html"
target="_blank"
rel="noopener noreferrer"
>
{t('gdprMessagePrivacyPolicy')}
</a>,
])}
</Typography>
<div className="onboarding-metametrics__buttons">
<Button type="secondary" onClick={onCancel}>
{t('noThanks')}
</Button>
<Button type="primary" onClick={onConfirm}>
{t('affirmAgree')}
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import OnboardingMetametrics from './metametrics';
export default {
title: 'Onboarding',
id: __filename,
};
export const OnboardingComponent = () => <OnboardingMetametrics />;

View File

@ -13,6 +13,8 @@ import {
ONBOARDING_PRIVACY_SETTINGS_ROUTE,
ONBOARDING_COMPLETION_ROUTE,
ONBOARDING_IMPORT_WITH_SRP_ROUTE,
ONBOARDING_PIN_EXTENSION_ROUTE,
ONBOARDING_METAMETRICS,
} from '../../helpers/constants/routes';
import {
getCompletedOnboarding,
@ -37,6 +39,8 @@ import PrivacySettings from './privacy-settings/privacy-settings';
import CreationSuccessful from './creation-successful/creation-successful';
import OnboardingWelcome from './welcome/welcome';
import ImportSRP from './import-srp/import-srp';
import OnboardingPinExtension from './pin-extension/pin-extension';
import MetaMetricsComponent from './metametrics/metametrics';
export default function OnboardingFlow() {
const [secretRecoveryPhrase, setSecretRecoveryPhrase] = useState('');
@ -51,13 +55,6 @@ export default function OnboardingFlow() {
const nextRoute = useSelector(getFirstTimeFlowTypeRoute);
useEffect(() => {
// For ONBOARDING_V2 dev purposes,
// Remove when ONBOARDING_V2 dev complete
if (process.env.ONBOARDING_V2) {
history.push(ONBOARDING_IMPORT_WITH_SRP_ROUTE);
return;
}
if (completedOnboarding && seedPhraseBackedUp) {
history.push(DEFAULT_ROUTE);
return;
@ -156,6 +153,14 @@ export default function OnboardingFlow() {
path={ONBOARDING_WELCOME_ROUTE}
component={OnboardingWelcome}
/>
<Route
path={ONBOARDING_PIN_EXTENSION_ROUTE}
component={OnboardingPinExtension}
/>
<Route
path={ONBOARDING_METAMETRICS}
component={MetaMetricsComponent}
/>
<Route exact path="*" component={OnboardingFlowSwitch} />
</Switch>
</div>

View File

@ -0,0 +1,23 @@
.onboarding-pin-extension {
max-width: 800px;
.control-dots .dot {
background: $ui-2;
box-shadow: none;
&.selected {
background: $ui-4;
}
}
&__diagram {
margin: 24px auto;
width: 799px;
height: 320px;
}
&__buttons {
max-width: 50%;
margin: 40px auto 0 auto;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Carousel } from 'react-responsive-carousel';
import Typography from '../../../components/ui/typography/typography';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Button from '../../../components/ui/button';
import {
TYPOGRAPHY,
FONT_WEIGHT,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import OnboardingPinBillboard from './pin-billboard';
export default function OnboardingPinExtension() {
const t = useI18nContext();
const [selectedIndex, setSelectedIndex] = useState(0);
const history = useHistory();
return (
<div className="onboarding-pin-extension">
<Typography
variant={TYPOGRAPHY.H2}
align={TEXT_ALIGN.CENTER}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('onboardingPinExtensionTitle')}
</Typography>
<Carousel
selectedItem={selectedIndex}
showThumbs={false}
showStatus={false}
showArrows={false}
>
<div>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription')}
</Typography>
<div className="onboarding-pin-extension__diagram">
<OnboardingPinBillboard />
</div>
</div>
<div>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription2')}
</Typography>
<Typography align={TEXT_ALIGN.CENTER}>
{t('onboardingPinExtensionDescription3')}
</Typography>
<img
src="/images/onboarding-pin-browser.svg"
width="799"
height="320"
alt=""
/>
</div>
</Carousel>
<div className="onboarding-pin-extension__buttons">
<Button
type="primary"
onClick={() => {
if (selectedIndex === 0) {
setSelectedIndex(1);
} else {
history.push(DEFAULT_ROUTE);
}
}}
>
{selectedIndex === 0 ? t('next') : t('done')}
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import OnboardingPinExtension from './pin-extension';
export default {
title: 'Onboarding',
id: __filename,
};
export const OnboardingComponent = () => <OnboardingPinExtension />;

View File

@ -10,6 +10,7 @@ import {
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
setCompletedOnboarding,
setFeatureFlag,
setUsePhishDetect,
setUseTokenDetection,
@ -33,6 +34,7 @@ export default function PrivacySettings() {
);
dispatch(setUsePhishDetect(usePhishingDetection));
dispatch(setUseTokenDetection(turnOnTokenDetection));
dispatch(setCompletedOnboarding());
history.push(ONBOARDING_PIN_EXTENSION_ROUTE);
};

View File

@ -2,8 +2,10 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from '../../../store/actions';
import { renderWithProvider } from '../../../../test/jest';
import {
renderWithProvider,
setBackgroundConnection,
} from '../../../../test/jest';
import PrivacySettings from './privacy-settings';
describe('Privacy Settings Onboarding View', () => {
@ -19,11 +21,15 @@ describe('Privacy Settings Onboarding View', () => {
const setFeatureFlagStub = jest.fn();
const setUsePhishDetectStub = jest.fn();
const setUseTokenDetectionStub = jest.fn();
const completeOnboardingStub = jest
.fn()
.mockImplementation(() => Promise.resolve());
actions._setBackgroundConnection({
setBackgroundConnection({
setFeatureFlag: setFeatureFlagStub,
setUsePhishDetect: setUsePhishDetectStub,
setUseTokenDetection: setUseTokenDetectionStub,
completeOnboarding: completeOnboardingStub,
});
it('should update preferences', () => {

View File

@ -13,7 +13,7 @@ import {
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { setFirstTimeFlowType } from '../../../store/actions';
import { INITIALIZE_METAMETRICS_OPT_IN_ROUTE } from '../../../helpers/constants/routes';
import { ONBOARDING_METAMETRICS } from '../../../helpers/constants/routes';
export default function OnboardingWelcome() {
const t = useI18nContext();
@ -23,12 +23,12 @@ export default function OnboardingWelcome() {
const onCreateClick = () => {
dispatch(setFirstTimeFlowType('create'));
history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE);
history.push(ONBOARDING_METAMETRICS);
};
const onImportClick = () => {
dispatch(setFirstTimeFlowType('import'));
history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE);
history.push(ONBOARDING_METAMETRICS);
};
return (

View File

@ -340,8 +340,8 @@ export default class Routes extends Component {
/>
<AccountMenu />
<div className="main-container-wrapper">
{isLoading && <Loading loadingMessage={loadMessage} />}
{!isLoading && isNetworkLoading && <LoadingNetwork />}
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
{this.renderRoutes()}
</div>
{isUnlocked ? <Alerts history={this.props.history} /> : null}

View File

@ -50,17 +50,20 @@ export default class SendContent extends Component {
return (
<PageContainerContent>
<div className="send-v2__form">
{gasError && this.renderError(gasError)}
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
{isAssetSendable === false &&
this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
{error && this.renderError(error)}
{warning && this.renderWarning()}
{gasError ? this.renderError(gasError) : null}
{isEthGasPrice
? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
: null}
{isAssetSendable === false
? this.renderError(UNSENDABLE_ASSET_ERROR_KEY)
: null}
{error ? this.renderError(error) : null}
{warning ? this.renderWarning() : null}
{this.maybeRenderAddContact()}
<SendAssetRow />
<SendAmountRow />
{networkOrAccountNotSupports1559 && <SendGasRow />}
{this.props.showHexData && <SendHexDataRow />}
{networkOrAccountNotSupports1559 ? <SendGasRow /> : null}
{this.props.showHexData ? <SendHexDataRow /> : null}
</div>
</PageContainerContent>
);

View File

@ -30,7 +30,7 @@ export default class SendRowWrapper extends Component {
<div className="send-v2__form-field-container">
<div className="send-v2__form-field">{formField}</div>
<div>
{showError && <SendRowErrorMessage errorType={errorType} />}
{showError ? <SendRowErrorMessage errorType={errorType} /> : null}
</div>
</div>
</div>
@ -50,7 +50,7 @@ export default class SendRowWrapper extends Component {
<div className="send-v2__form-row">
<div className="send-v2__form-label">
{label}
{showError && <SendRowErrorMessage errorType={errorType} />}
{showError ? <SendRowErrorMessage errorType={errorType} /> : null}
{customLabelContent}
</div>
<div className="send-v2__form-field">{formField}</div>

View File

@ -533,7 +533,7 @@ export default class AdvancedTab extends PureComponent {
return (
<div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>}
{warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderStateLogs()}
{this.renderMobileSync()}
{this.renderResetAccount()}

View File

@ -136,7 +136,9 @@ export default class ContactListTab extends Component {
<div className="address-book-wrapper">
{this.renderAddressBookContent()}
{this.renderContactContent()}
{!addingContact && addressBook.length > 0 && this.renderAddButton()}
{!addingContact && addressBook.length > 0
? this.renderAddButton()
: null}
</div>
);
}

View File

@ -25,8 +25,6 @@ export default class EditContact extends PureComponent {
memo: PropTypes.string,
viewRoute: PropTypes.string,
listRoute: PropTypes.string,
setAccountLabel: PropTypes.func,
showingMyAccounts: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -52,8 +50,6 @@ export default class EditContact extends PureComponent {
memo,
name,
removeFromAddressBook,
setAccountLabel,
showingMyAccounts,
viewRoute,
} = this.props;
@ -65,18 +61,16 @@ export default class EditContact extends PureComponent {
<div className="settings-page__content-row address-book__edit-contact">
<div className="settings-page__header address-book__header--edit">
<Identicon address={address} diameter={60} />
{showingMyAccounts ? null : (
<Button
type="link"
className="settings-page__address-book-button"
onClick={async () => {
await removeFromAddressBook(chainId, address);
history.push(listRoute);
}}
>
{t('deleteAccount')}
</Button>
)}
<Button
type="link"
className="settings-page__address-book-button"
onClick={async () => {
await removeFromAddressBook(chainId, address);
history.push(listRoute);
}}
>
{t('deleteAccount')}
</Button>
</div>
<div className="address-book__edit-contact__content">
<div className="address-book__view-contact__group">
@ -157,12 +151,6 @@ export default class EditContact extends PureComponent {
this.state.newName || name,
this.state.newMemo || memo,
);
if (showingMyAccounts) {
setAccountLabel(
this.state.newAddress,
this.state.newName || name,
);
}
history.push(listRoute);
} else {
this.setState({ error: this.context.t('invalidAddress') });
@ -174,9 +162,6 @@ export default class EditContact extends PureComponent {
this.state.newName || name,
this.state.newMemo || memo,
);
if (showingMyAccounts) {
setAccountLabel(address, this.state.newName || name);
}
history.push(listRoute);
}
}}

View File

@ -9,7 +9,6 @@ import {
import {
addToAddressBook,
removeFromAddressBook,
setAccountLabel,
} from '../../../../store/actions';
import EditContact from './edit-contact.component';
@ -44,8 +43,6 @@ const mapDispatchToProps = (dispatch) => {
dispatch(addToAddressBook(recipient, nickname, memo)),
removeFromAddressBook: (chainId, addressToRemove) =>
dispatch(removeFromAddressBook(chainId, addressToRemove)),
setAccountLabel: (address, label) =>
dispatch(setAccountLabel(address, label)),
};
};

View File

@ -244,7 +244,7 @@ export default class NetworksTab extends PureComponent {
return (
<div className="networks-tab__body">
{isFullScreen && this.renderSubHeader()}
{isFullScreen ? this.renderSubHeader() : null}
<div className="networks-tab__content">
{this.renderNetworksTabContent()}
{!isFullScreen && !shouldRenderNetworkForm ? (

View File

@ -146,7 +146,7 @@ export default class SecurityTab extends PureComponent {
return (
<div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>}
{warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderSeedWords()}
{this.renderIncomingTransactionsOptIn()}
{this.renderPhishingDetectionToggle()}

View File

@ -212,7 +212,7 @@ export default class SettingsTab extends PureComponent {
return (
<div className="settings-page__body">
{warning && <div className="settings-tab__error">{warning}</div>}
{warning ? <div className="settings-tab__error">{warning}</div> : null}
{this.renderCurrentConversion()}
{this.renderUsePrimaryCurrencyOptions()}
{this.renderCurrentLocale()}

View File

@ -272,7 +272,7 @@ export default function AwaitingSwap({
<div className="awaiting-swap__main-descrption">{descriptionText}</div>
{content}
</div>
{!errorKey && swapComplete && <MakeAnotherSwap />}
{!errorKey && swapComplete ? <MakeAnotherSwap /> : null}
<SwapsFooter
onSubmit={async () => {
if (errorKey === OFFLINE_FOR_MAINTENANCE) {

View File

@ -52,13 +52,16 @@ export default function ItemList({
// If there is a token for import based on a contract address, it's the only one in the list.
const hasTokenForImport = results.length === 1 && results[0].notImported;
const placeholder = Placeholder ? (
<Placeholder searchQuery={searchQuery} />
) : null;
return results.length === 0 ? (
Placeholder && <Placeholder searchQuery={searchQuery} />
placeholder
) : (
<div className="searchable-item-list">
{listTitle && (
{listTitle ? (
<div className="searchable-item-list__title">{listTitle}</div>
)}
) : null}
<div
className={classnames(
'searchable-item-list__list-container',
@ -100,43 +103,43 @@ export default function ItemList({
onKeyUp={(e) => e.key === 'Enter' && onClick()}
key={`searchable-item-list-item-${i}`}
>
{(iconUrl || primaryLabel) && (
{iconUrl || primaryLabel ? (
<UrlIcon url={iconUrl} name={primaryLabel} />
)}
{!(iconUrl || primaryLabel) && identiconAddress && (
) : null}
{!(iconUrl || primaryLabel) && identiconAddress ? (
<div className="searchable-item-list__identicon">
<Identicon address={identiconAddress} diameter={24} />
</div>
)}
{IconComponent && <IconComponent />}
) : null}
{IconComponent ? <IconComponent /> : null}
<div className="searchable-item-list__labels">
<div className="searchable-item-list__item-labels">
{primaryLabel && (
{primaryLabel ? (
<span className="searchable-item-list__primary-label">
{primaryLabel}
</span>
)}
{secondaryLabel && (
) : null}
{secondaryLabel ? (
<span className="searchable-item-list__secondary-label">
{secondaryLabel}
</span>
)}
) : null}
</div>
{!hideRightLabels &&
(rightPrimaryLabel || rightSecondaryLabel) && (
<div className="searchable-item-list__right-labels">
{rightPrimaryLabel && (
<span className="searchable-item-list__right-primary-label">
{rightPrimaryLabel}
</span>
)}
{rightSecondaryLabel && (
<span className="searchable-item-list__right-secondary-label">
{rightSecondaryLabel}
</span>
)}
</div>
)}
(rightPrimaryLabel || rightSecondaryLabel) ? (
<div className="searchable-item-list__right-labels">
{rightPrimaryLabel ? (
<span className="searchable-item-list__right-primary-label">
{rightPrimaryLabel}
</span>
) : null}
{rightSecondaryLabel ? (
<span className="searchable-item-list__right-secondary-label">
{rightSecondaryLabel}
</span>
) : null}
</div>
) : null}
</div>
{result.notImported && (
<Button type="confirm" onClick={onClick}>

View File

@ -10,7 +10,7 @@ import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
import { useEqualityCheck } from '../../../hooks/useEqualityCheck';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { usePrevious } from '../../../hooks/usePrevious';
import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs';
import { useGasFeeInputs } from '../../../hooks/gasFeeInput/useGasFeeInputs';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import FeeCard from '../fee-card';
import EditGasPopover from '../../../components/app/edit-gas-popover/edit-gas-popover.component';

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