diff --git a/ui/components/ui/ui-components.scss b/ui/components/ui/ui-components.scss
index 34c435acd..4c0e871a1 100644
--- a/ui/components/ui/ui-components.scss
+++ b/ui/components/ui/ui-components.scss
@@ -1,5 +1,6 @@
/** Please import your files in alphabetical order **/
@import 'account-mismatch-warning/index';
+@import 'account-list/index';
@import 'actionable-message/index';
@import 'alert-circle-icon/index';
@import 'alert/index';
@@ -45,6 +46,7 @@
@import 'readonly-input/index';
@import 'sender-to-recipient/index';
@import 'snackbar/index';
+@import 'site-origin/index';
@import 'slider/index';
@import 'tabs/index';
@import 'toggle-button/index';
@@ -52,6 +54,7 @@
@import 'tooltip/index';
@import 'truncated-definition-list/truncated-definition-list';
@import 'typography/typography';
+@import 'textarea/index';
@import 'unit-input/index';
@import 'url-icon/index';
@import 'update-nickname-popover/index';
diff --git a/ui/components/ui/update-nickname-popover/update-nickname-popover.stories.js b/ui/components/ui/update-nickname-popover/update-nickname-popover.stories.js
index ce9c34aca..4e870b01f 100644
--- a/ui/components/ui/update-nickname-popover/update-nickname-popover.stories.js
+++ b/ui/components/ui/update-nickname-popover/update-nickname-popover.stories.js
@@ -5,7 +5,7 @@ import Button from '../button';
import UpdateNicknamePopover from '.';
export default {
- title: 'UpdateNickname',
+ title: 'Components/UI/UpdateNickname',
id: __filename,
};
diff --git a/ui/components/ui/url-icon/url-icon.stories.js b/ui/components/ui/url-icon/url-icon.stories.js
index 630fefba3..ed9144fec 100644
--- a/ui/components/ui/url-icon/url-icon.stories.js
+++ b/ui/components/ui/url-icon/url-icon.stories.js
@@ -3,7 +3,7 @@ import { text } from '@storybook/addon-knobs';
import UrlIcon from './url-icon';
export default {
- title: 'UrlIcon',
+ title: 'Components/UI/UrlIcon',
id: __filename,
};
diff --git a/ui/contexts/transaction-modal.js b/ui/contexts/transaction-modal.js
index bf16ecf09..7b864b1dc 100644
--- a/ui/contexts/transaction-modal.js
+++ b/ui/contexts/transaction-modal.js
@@ -12,6 +12,7 @@ export const TransactionModalContextProvider = ({
actionKey,
children,
methodData,
+ captureEventEnabled = true,
}) => {
const [openModals, setOpenModals] = useState([]);
const metricsEvent = useMetaMetricsContext();
@@ -28,7 +29,7 @@ export const TransactionModalContextProvider = ({
recipientKnown: null,
functionType:
actionKey ||
- getMethodName(methodData.name) ||
+ getMethodName(methodData?.name) ||
TRANSACTION_TYPES.CONTRACT_INTERACTION,
origin,
},
@@ -49,7 +50,7 @@ export const TransactionModalContextProvider = ({
const openModal = (modalName) => {
if (openModals.includes(modalName)) return;
- captureEvent();
+ captureEventEnabled && captureEvent();
const modals = [...openModals];
modals.push(modalName);
setOpenModals(modals);
@@ -77,4 +78,5 @@ TransactionModalContextProvider.propTypes = {
actionKey: PropTypes.string,
children: PropTypes.node.isRequired,
methodData: PropTypes.object,
+ captureEventEnabled: PropTypes.bool,
};
diff --git a/ui/css/design-system/attributes.scss b/ui/css/design-system/attributes.scss
index 4a187a949..3062fdb6e 100644
--- a/ui/css/design-system/attributes.scss
+++ b/ui/css/design-system/attributes.scss
@@ -1,28 +1,11 @@
-$align-items:
- baseline,
- center,
- flex-end,
- flex-start,
- stretch;
+$align-items: baseline, center, flex-end, flex-start, stretch;
-$justify-content:
- center,
- flex-end,
- flex-start,
- space-around,
- space-between,
+$justify-content: center, flex-end, flex-start, space-around, space-between,
space-evenly;
-$flex-direction:
- row,
- row-reverse,
- column,
- column-reverse;
+$flex-direction: row, row-reverse, column, column-reverse;
-$flex-wrap:
- wrap,
- wrap-reverse,
- nowrap;
+$flex-wrap: wrap, wrap-reverse, nowrap;
$fractions: (
1\/2: 50%,
@@ -50,36 +33,21 @@ $fractions: (
8\/12: 66.666667%,
9\/12: 75%,
10\/12: 83.333333%,
- 11\/12: 91.666667%,
+ 11\/12: 91.666667%
);
-$sizes-numeric:
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 10,
- 11,
- 12;
+$sizes-numeric: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12;
-$sizes-strings:
- xs,
- sm,
- md,
- lg,
- xl,
- none;
+$sizes-strings: xs, sm, md, lg, xl, none;
$border-style: solid, double, none, dashed, dotted;
$directions: top, right, bottom, left;
$display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item;
$text-align: left, right, center, justify, end;
+$overflow-wrap: normal, break-word;
$font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900;
$font-style: normal, italic, oblique;
$font-size: 10px, 12px;
+
+// textarea
+$resize: none, both, horizontal, vertical, initial, inherit;
diff --git a/ui/css/design-system/colors.scss b/ui/css/design-system/colors.scss
index b66c07690..e8b7bd56a 100644
--- a/ui/css/design-system/colors.scss
+++ b/ui/css/design-system/colors.scss
@@ -122,6 +122,7 @@ $color-map: (
'white': $ui-white,
'black': $ui-black,
'grey': $ui-grey,
+ 'neutral-grey': $neutral-grey,
'primary-1': $primary-1,
'primary-2': $primary-2,
'primary-3': $primary-3,
diff --git a/ui/css/design-system/typography.scss b/ui/css/design-system/typography.scss
index 12b7d37ea..36544cd19 100644
--- a/ui/css/design-system/typography.scss
+++ b/ui/css/design-system/typography.scss
@@ -1,6 +1,7 @@
$fa-font-path: '/fonts/fontawesome';
@import '../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome';
+@import '../../../node_modules/@fortawesome/fontawesome-free/scss/brands';
@import '../../../node_modules/@fortawesome/fontawesome-free/scss/solid';
@import '../../../node_modules/@fortawesome/fontawesome-free/scss/regular';
diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js
index 9578826e1..c4eb95829 100644
--- a/ui/ducks/metamask/metamask.js
+++ b/ui/ducks/metamask/metamask.js
@@ -256,6 +256,34 @@ export const getUnconnectedAccountAlertShown = (state) =>
export const getTokens = (state) => state.metamask.tokens;
+export function getCollectiblesDetectionNoticeDismissed(state) {
+ return state.metamask.collectiblesDetectionNoticeDismissed;
+}
+
+export const getCollectibles = (state) => {
+ const {
+ metamask: {
+ allCollectibles,
+ provider: { chainId },
+ selectedAddress,
+ },
+ } = state;
+
+ return allCollectibles?.[selectedAddress]?.[chainId] ?? [];
+};
+
+export const getCollectibleContracts = (state) => {
+ const {
+ metamask: {
+ allCollectibleContracts,
+ provider: { chainId },
+ selectedAddress,
+ },
+ } = state;
+
+ return allCollectibleContracts?.[selectedAddress]?.[chainId] ?? [];
+};
+
export function getBlockGasLimit(state) {
return state.metamask.currentBlockGasLimit;
}
diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js
index 690092173..83d76ac99 100644
--- a/ui/ducks/send/send.js
+++ b/ui/ducks/send/send.js
@@ -419,6 +419,7 @@ export const initializeSendState = createAsyncThunk(
send: { asset, stage, recipient, amount, draftTransaction },
metamask,
} = state;
+
// First determine the correct from address. For new sends this is always
// the currently selected account and switching accounts switches the from
// address. If editing an existing transaction (by clicking 'edit' on the
@@ -707,6 +708,7 @@ const slice = createSlice({
state.recipient.nickname = action.payload.nickname;
state.draftTransaction.id = action.payload.id;
state.draftTransaction.txParams.from = action.payload.from;
+ state.draftTransaction.userInputHexData = action.payload.data;
slice.caseReducers.updateDraftTransaction(state);
},
/**
@@ -1601,6 +1603,7 @@ export function editTransaction(
const { txParams } = transaction;
if (assetType === ASSET_TYPES.NATIVE) {
const {
+ data,
from,
gas: gasLimit,
gasPrice,
@@ -1610,6 +1613,7 @@ export function editTransaction(
const nickname = getAddressBookEntry(state, address)?.name ?? '';
await dispatch(
actions.editTransaction({
+ data,
id: transactionId,
gasLimit,
gasPrice,
@@ -1624,7 +1628,13 @@ export function editTransaction(
`send/editTransaction dispatched with assetType 'TOKEN' but missing assetData or assetDetails parameter`,
);
} else {
- const { from, to: tokenAddress, gas: gasLimit, gasPrice } = txParams;
+ const {
+ data,
+ from,
+ to: tokenAddress,
+ gas: gasLimit,
+ gasPrice,
+ } = txParams;
const tokenAmountInDec = getTokenValueParam(tokenData);
const address = getTokenAddressParam(tokenData);
const nickname = getAddressBookEntry(state, address)?.name ?? '';
@@ -1645,6 +1655,7 @@ export function editTransaction(
await dispatch(
actions.editTransaction({
+ data,
id: transactionId,
gasLimit,
gasPrice,
diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js
index aa7f7e25c..286ade366 100644
--- a/ui/ducks/send/send.test.js
+++ b/ui/ducks/send/send.test.js
@@ -2077,6 +2077,7 @@ describe('Send Slice', () => {
1: {
id: 1,
txParams: {
+ data: '',
from: '0xAddress',
to: '0xRecipientAddress',
gas: GAS_LIMITS.SIMPLE,
@@ -2107,6 +2108,7 @@ describe('Send Slice', () => {
expect(actionResult[0].payload).toStrictEqual({
address: '0xRecipientAddress',
amount: '0xde0b6b3a7640000',
+ data: '',
from: '0xAddress',
gasLimit: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
@@ -2154,6 +2156,7 @@ describe('Send Slice', () => {
1: {
id: 1,
txParams: {
+ data: '',
from: '0xAddress',
to: '0xTokenAddress',
gas: GAS_LIMITS.SIMPLE,
@@ -2241,6 +2244,7 @@ describe('Send Slice', () => {
expect(actionResult[6].payload).toStrictEqual({
address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase
amount: '0x3a98',
+ data: '',
from: '0xAddress',
gasLimit: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00',
diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js
index ae26f9d28..893e332da 100644
--- a/ui/helpers/constants/design-system.js
+++ b/ui/helpers/constants/design-system.js
@@ -11,6 +11,7 @@ export const COLORS = {
UI4: 'ui-4',
BLACK: 'black',
GREY: 'grey',
+ NEUTRAL_GREY: 'neutral-grey',
WHITE: 'white',
PRIMARY1: 'primary-1',
PRIMARY2: 'primary-2',
@@ -171,6 +172,11 @@ export const FONT_WEIGHT = {
900: 900,
};
+export const OVERFLOW_WRAP = {
+ BREAK_WORD: 'break-word',
+ NORMAL: 'normal',
+};
+
export const FONT_STYLE = {
ITALIC: 'italic',
NORMAL: 'normal',
@@ -183,3 +189,12 @@ export const SEVERITIES = {
INFO: 'info',
SUCCESS: 'success',
};
+
+export const RESIZE = {
+ NONE: 'none',
+ BOTH: 'both',
+ HORIZONTAL: 'horizontal',
+ VERTICAL: 'vertical',
+ INITIAL: 'initial',
+ INHERIT: 'inherit',
+};
diff --git a/ui/helpers/constants/gas.js b/ui/helpers/constants/gas.js
index 726057342..4ca2066b9 100644
--- a/ui/helpers/constants/gas.js
+++ b/ui/helpers/constants/gas.js
@@ -37,5 +37,6 @@ export const PRIORITY_LEVEL_ICON_MAP = {
medium: '🦊',
high: '🦍',
dappSuggested: '🌐',
+ swapSuggested: '🔄',
custom: '⚙',
};
diff --git a/ui/helpers/constants/routes.js b/ui/helpers/constants/routes.js
index 541dad5c1..1de62a7ff 100644
--- a/ui/helpers/constants/routes.js
+++ b/ui/helpers/constants/routes.js
@@ -93,7 +93,7 @@ const PATH_NAME_MAP = {
[DEFAULT_ROUTE]: 'Home',
[UNLOCK_ROUTE]: 'Unlock Page',
[LOCK_ROUTE]: 'Lock Page',
- [`${ASSET_ROUTE}/:asset`]: `Asset Page`,
+ [`${ASSET_ROUTE}/:asset/:id`]: `Asset Page`,
[SETTINGS_ROUTE]: 'Settings Page',
[GENERAL_ROUTE]: 'General Settings Page',
[ADVANCED_ROUTE]: 'Advanced Settings Page',
diff --git a/ui/helpers/utils/common.util.test.js b/ui/helpers/utils/common.util.test.js
index 84e5c22b2..9230dd2da 100644
--- a/ui/helpers/utils/common.util.test.js
+++ b/ui/helpers/utils/common.util.test.js
@@ -5,21 +5,21 @@ describe('Common utils', () => {
it('should return a capitalized string from a camel-cased string', () => {
const tests = [
{
- test: undefined,
+ input: undefined,
expected: '',
},
{
- test: '',
+ input: '',
expected: '',
},
{
- test: 'thisIsATest',
+ input: 'thisIsATest',
expected: 'This Is A Test',
},
];
- tests.forEach(({ test, expected }) => {
- expect(utils.camelCaseToCapitalize(test)).toStrictEqual(expected);
+ tests.forEach(({ input, expected }) => {
+ expect(utils.camelCaseToCapitalize(input)).toStrictEqual(expected);
});
});
});
diff --git a/ui/helpers/utils/i18n-helper.js b/ui/helpers/utils/i18n-helper.js
index d9d7a35a8..cfc385f0c 100644
--- a/ui/helpers/utils/i18n-helper.js
+++ b/ui/helpers/utils/i18n-helper.js
@@ -32,7 +32,7 @@ export const getMessage = (localeCode, localeMessages, key, substitutions) => {
);
Sentry.captureException(missingMessageErrors[key]);
log.error(missingMessageErrors[key]);
- if (process.env.IN_TEST === 'true') {
+ if (process.env.IN_TEST) {
throw missingMessageErrors[key];
}
}
diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js
index f7d839d08..cbbda4398 100644
--- a/ui/helpers/utils/util.js
+++ b/ui/helpers/utils/util.js
@@ -3,6 +3,7 @@ import abi from 'human-standard-token-abi';
import BigNumber from 'bignumber.js';
import * as ethUtil from 'ethereumjs-util';
import { DateTime } from 'luxon';
+import { util } from '@metamask/controllers';
import { addHexPrefix } from '../../../app/scripts/lib/util';
import {
GOERLI_CHAIN_ID,
@@ -261,6 +262,21 @@ export function stripHttpsScheme(urlString) {
return urlString.replace(/^https:\/\//u, '');
}
+/**
+ * Strips `https` schemes from URL strings, if the URL does not have a port.
+ * This is useful
+ *
+ * @param {string} urlString - The URL string to strip the scheme from.
+ * @returns {string} The URL string, without the scheme, if it was stripped.
+ */
+export function stripHttpsSchemeWithoutPort(urlString) {
+ if (getURL(urlString).port) {
+ return urlString;
+ }
+
+ return stripHttpsScheme(urlString);
+}
+
/**
* Checks whether a URL-like value (object or string) is an extension URL.
*
@@ -428,3 +444,117 @@ export const toHumanReadableTime = (t, milliseconds) => {
export function clearClipboard() {
window.navigator.clipboard.writeText('');
}
+
+const solidityTypes = () => {
+ const types = [
+ 'bool',
+ 'address',
+ 'string',
+ 'bytes',
+ 'int',
+ 'uint',
+ 'fixed',
+ 'ufixed',
+ ];
+
+ const ints = Array.from(new Array(32)).map(
+ (_, index) => `int${(index + 1) * 8}`,
+ );
+ const uints = Array.from(new Array(32)).map(
+ (_, index) => `uint${(index + 1) * 8}`,
+ );
+ const bytes = Array.from(new Array(32)).map(
+ (_, index) => `bytes${index + 1}`,
+ );
+
+ /**
+ * fixed and ufixed
+ * This value type also can be declared keywords such as ufixedMxN and fixedMxN.
+ * The M represents the amount of bits that the type takes,
+ * with N representing the number of decimal points that are available.
+ * M has to be divisible by 8, and a number from 8 to 256.
+ * N has to be a value between 0 and 80, also being inclusive.
+ */
+ const fixedM = Array.from(new Array(32)).map(
+ (_, index) => `fixed${(index + 1) * 8}`,
+ );
+ const ufixedM = Array.from(new Array(32)).map(
+ (_, index) => `ufixed${(index + 1) * 8}`,
+ );
+ const fixed = Array.from(new Array(80)).map((_, index) =>
+ fixedM.map((aFixedM) => `${aFixedM}x${index + 1}`),
+ );
+ const ufixed = Array.from(new Array(80)).map((_, index) =>
+ ufixedM.map((auFixedM) => `${auFixedM}x${index + 1}`),
+ );
+
+ return [
+ ...types,
+ ...ints,
+ ...uints,
+ ...bytes,
+ ...fixed.flat(),
+ ...ufixed.flat(),
+ ];
+};
+
+export const sanitizeMessage = (msg, baseType, types) => {
+ if (!types) {
+ throw new Error(`Invalid types definition`);
+ }
+
+ const baseTypeDefinitions = types[baseType];
+ if (!baseTypeDefinitions) {
+ throw new Error(`Invalid primary type definition`);
+ }
+
+ const sanitizedMessage = {};
+ const msgKeys = Object.keys(msg);
+ msgKeys.forEach((msgKey) => {
+ const definedType = Object.values(baseTypeDefinitions).find(
+ (baseTypeDefinition) => baseTypeDefinition.name === msgKey,
+ );
+
+ if (!definedType) {
+ return;
+ }
+
+ // key has a type. check if the definedType is also a type
+ const nestedType = definedType.type.replace(/\[\]$/u, '');
+ const nestedTypeDefinition = types[nestedType];
+
+ if (nestedTypeDefinition) {
+ if (definedType.type.endsWith('[]') > 0) {
+ // nested array
+ sanitizedMessage[msgKey] = msg[msgKey].map((value) =>
+ sanitizeMessage(value, nestedType, types),
+ );
+ } else {
+ // nested object
+ sanitizedMessage[msgKey] = sanitizeMessage(
+ msg[msgKey],
+ definedType.type,
+ types,
+ );
+ }
+ } else {
+ // check if it's a valid solidity type
+ const isSolidityType = solidityTypes().includes(nestedType);
+ if (isSolidityType) {
+ sanitizedMessage[msgKey] = msg[msgKey];
+ }
+ }
+ });
+ return sanitizedMessage;
+};
+
+export function getAssetImageURL(image, ipfsGateway) {
+ if (!image || !ipfsGateway || typeof image !== 'string') {
+ return '';
+ }
+
+ if (image.startsWith('ipfs://')) {
+ return util.getFormattedIpfsUrl(ipfsGateway, image, true);
+ }
+ return image;
+}
diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js
index 1f9084865..4dd6ab632 100644
--- a/ui/helpers/utils/util.test.js
+++ b/ui/helpers/utils/util.test.js
@@ -340,4 +340,90 @@ describe('util', () => {
expect(util.toHumanReadableTime(t, 7200000)).toStrictEqual('2 hrs');
});
});
+ describe('sanitizeMessage', () => {
+ let message;
+ let primaryType;
+ let types;
+
+ beforeEach(() => {
+ message = {
+ contents: 'Hello, Bob!',
+ from: {
+ name: 'Cow',
+ wallets: [
+ '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+ '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
+ ],
+ },
+ to: [
+ {
+ name: 'Bob',
+ wallets: [
+ '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+ '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
+ '0xB0B0b0b0b0b0B000000000000000000000000000',
+ ],
+ },
+ ],
+ };
+ primaryType = 'Mail';
+ types = {
+ EIP712Domain: [
+ { name: 'name', type: 'string' },
+ { name: 'version', type: 'string' },
+ { name: 'chainId', type: 'uint256' },
+ { name: 'verifyingContract', type: 'address' },
+ ],
+ Mail: [
+ { name: 'from', type: 'Person' },
+ { name: 'to', type: 'Person[]' },
+ { name: 'contents', type: 'string' },
+ ],
+ Person: [
+ { name: 'name', type: 'string' },
+ { name: 'wallets', type: 'address[]' },
+ ],
+ };
+ });
+
+ it('should throw an error if types is undefined', () => {
+ expect(() =>
+ util.sanitizeMessage(message, primaryType, undefined),
+ ).toThrow('Invalid types definition');
+ });
+
+ it('should throw an error if base type is not defined', () => {
+ expect(() => util.sanitizeMessage(message, undefined, types)).toThrow(
+ 'Invalid primary type definition',
+ );
+ });
+
+ it('should return parsed message if types is defined', () => {
+ const result = util.sanitizeMessage(message, primaryType, types);
+ expect(result.contents).toStrictEqual('Hello, Bob!');
+ expect(result.from.name).toStrictEqual('Cow');
+ expect(result.from.wallets).toHaveLength(2);
+ expect(result.to).toHaveLength(1);
+ expect(result.to[0].name).toStrictEqual('Bob');
+ expect(result.to[0].wallets).toHaveLength(3);
+ });
+
+ it('should return ignore message data with unknown types', () => {
+ message.do_not_display = 'one';
+ message.do_not_display_2 = {
+ do_not_display: 'two',
+ };
+
+ // result will NOT contain the do_not_displays because type definition
+ const result = util.sanitizeMessage(message, primaryType, types);
+ expect(result.contents).toStrictEqual('Hello, Bob!');
+ expect(result.from.name).toStrictEqual('Cow');
+ expect(result.from.wallets).toHaveLength(2);
+ expect(result.to).toHaveLength(1);
+ expect(result.to[0].name).toStrictEqual('Bob');
+ expect(result.to[0].wallets).toHaveLength(3);
+ expect(result.do_not_display).toBeUndefined();
+ expect(result.do_not_display_2).toBeUndefined();
+ });
+ });
});
diff --git a/ui/hooks/gasFeeInput/test-utils.js b/ui/hooks/gasFeeInput/test-utils.js
index 039553c74..117d4f7cc 100644
--- a/ui/hooks/gasFeeInput/test-utils.js
+++ b/ui/hooks/gasFeeInput/test-utils.js
@@ -20,6 +20,10 @@ import {
import { ETH } from '../../helpers/constants/common';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
+import {
+ getCustomMaxFeePerGas,
+ getCustomMaxPriorityFeePerGas,
+} from '../../ducks/swaps/swaps';
// Why this number?
// 20 gwei * 21000 gasLimit = 420,000 gwei
@@ -122,6 +126,12 @@ export const generateUseSelectorRouter = ({
balance: '0x440aa47cc2556',
};
}
+ if (selector === getCustomMaxFeePerGas) {
+ return '0x5208';
+ }
+ if (selector === getCustomMaxPriorityFeePerGas) {
+ return '0x5208';
+ }
if (selector === checkNetworkAndAccountSupports1559) {
return checkNetworkAndAccountSupports1559Response;
}
diff --git a/ui/hooks/gasFeeInput/useGasFeeErrors.js b/ui/hooks/gasFeeInput/useGasFeeErrors.js
index 213b66b2d..686f1d28c 100644
--- a/ui/hooks/gasFeeInput/useGasFeeErrors.js
+++ b/ui/hooks/gasFeeInput/useGasFeeErrors.js
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import {
conversionLessThan,
@@ -249,7 +249,7 @@ export function useGasFeeErrors({
[gasErrors, gasWarnings],
);
- const { balance: ethBalance } = useSelector(getSelectedAccount);
+ const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual);
const balanceError = hasBalanceError(
minimumCostInHexWei,
transaction,
diff --git a/ui/hooks/gasFeeInput/useGasFeeInputs.js b/ui/hooks/gasFeeInput/useGasFeeInputs.js
index 49864193b..e2d7af7a6 100644
--- a/ui/hooks/gasFeeInput/useGasFeeInputs.js
+++ b/ui/hooks/gasFeeInput/useGasFeeInputs.js
@@ -23,8 +23,20 @@ import { useMaxPriorityFeePerGasInput } from './useMaxPriorityFeePerGasInput';
import { useGasEstimates } from './useGasEstimates';
import { useTransactionFunctions } from './useTransactionFunctions';
-// eslint-disable-next-line prefer-destructuring
-const EIP_1559_V2 = process.env.EIP_1559_V2;
+/**
+ * In EIP_1559_V2 implementation as used by useGasfeeInputContext() the use of this hook is evolved.
+ * It is no longer used to keep transient state of advance gas fee inputs.
+ * Transient state of inputs is maintained locally in /ui/components/app/advance-gas-fee-popover component.
+ *
+ * This hook is used now as source of shared data about transaction, it shares details of gas fee in transaction,
+ * estimate used, is EIP-1559 supported and other details. It also have methods to update transaction.
+ *
+ * Transaction is used as single source of truth and as transaction is updated the fields shared by hook are
+ * also updated using useEffect hook.
+ *
+ * It will be useful to plan a task to create a new hook of this shared information from this hook.
+ * Methods like setEstimateToUse, onManualChange are deprecated in context of EIP_1559_V2 implementation.
+ */
/**
* @typedef {Object} GasFeeInputReturnType
@@ -77,14 +89,16 @@ export function useGasFeeInputs(
minimumGasLimit = '0x5208',
editGasMode = EDIT_GAS_MODES.MODIFY_IN_PLACE,
) {
- // eslint-disable-next-line prefer-destructuring
const EIP_1559_V2_ENABLED =
+ // This is a string in unit tests but is a boolean in the browser
process.env.EIP_1559_V2 === true || process.env.EIP_1559_V2 === 'true';
const supportsEIP1559 =
useSelector(checkNetworkAndAccountSupports1559) &&
!isLegacyTransaction(transaction?.txParams);
+ const supportsEIP1559V2 = supportsEIP1559 && EIP_1559_V2_ENABLED;
+
// 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.
@@ -115,27 +129,31 @@ export function useGasFeeInputs(
return PRIORITY_LEVELS.CUSTOM;
});
+ const [gasLimit, setGasLimit] = useState(() =>
+ Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
+ );
+
/**
* In EIP-1559 V2 designs change to gas estimate is always updated to transaction
* Thus callback setEstimateToUse can be deprecate in favour of this useEffect
* so that transaction is source of truth whenever possible.
*/
useEffect(() => {
- if (EIP_1559_V2 && transaction?.userFeeLevel) {
- setEstimateUsed(transaction?.userFeeLevel);
- setInternalEstimateToUse(transaction?.userFeeLevel);
+ if (supportsEIP1559V2) {
+ if (transaction?.userFeeLevel) {
+ setEstimateUsed(transaction?.userFeeLevel);
+ setInternalEstimateToUse(transaction?.userFeeLevel);
+ }
+ setGasLimit(Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')));
}
}, [
setEstimateUsed,
+ setGasLimit,
setInternalEstimateToUse,
+ supportsEIP1559V2,
transaction,
- userPrefersAdvancedGas,
]);
- const [gasLimit, setGasLimit] = useState(() =>
- Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
- );
-
const {
gasPrice,
setGasPrice,
@@ -152,12 +170,12 @@ export function useGasFeeInputs(
maxFeePerGasFiat,
setMaxFeePerGas,
} = useMaxFeePerGasInput({
- EIP_1559_V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
+ supportsEIP1559V2,
transaction,
});
@@ -166,11 +184,11 @@ export function useGasFeeInputs(
maxPriorityFeePerGasFiat,
setMaxPriorityFeePerGas,
} = useMaxPriorityFeePerGasInput({
- EIP_1559_V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
+ supportsEIP1559V2,
transaction,
});
@@ -230,6 +248,7 @@ export function useGasFeeInputs(
updateTransactionUsingGasFeeEstimates,
} = useTransactionFunctions({
defaultEstimateToUse,
+ editGasMode,
gasFeeEstimates,
gasLimit,
transaction,
@@ -295,6 +314,7 @@ export function useGasFeeInputs(
setGasPrice,
gasLimit,
setGasLimit,
+ editGasMode,
estimateToUse,
setEstimateToUse,
estimatedMinimumFiat,
@@ -315,24 +335,10 @@ export function useGasFeeInputs(
gasWarnings,
hasGasErrors,
hasSimulationError,
+ minimumGasLimitDec: hexToDecimal(minimumGasLimit),
supportsEIP1559,
- supportsEIP1559V2: supportsEIP1559 && EIP_1559_V2_ENABLED,
+ supportsEIP1559V2,
updateTransaction,
updateTransactionUsingGasFeeEstimates,
};
}
-
-/**
- * In EIP_1559_V2 implementation as used by useGasfeeInputContext() the use of this hook is evolved.
- * It is no longer used to keep transient state of advance gas fee inputs.
- * Transient state of inputs is maintained locally in /ui/components/app/advance-gas-fee-popover component.
- *
- * This hook is used now as source of shared data about transaction, it shares details of gas fee in transaction,
- * estimate used, is EIP-1559 supported and other details. It also have methods to update transaction.
- *
- * Transaction is used as single source of truth and as transaction is updated the fields shared by hook are
- * also updated using useEffect hook.
- *
- * It will be useful to plan a task to create a new hook of this shared information from this hook.
- * Methods like setEstimateToUse, onManualChange are deprecated in context of EIP_1559_V2 implementation.
- */
diff --git a/ui/hooks/gasFeeInput/useGasFeeInputs.test.js b/ui/hooks/gasFeeInput/useGasFeeInputs.test.js
index f4bf4f908..8b3f1e9e3 100644
--- a/ui/hooks/gasFeeInput/useGasFeeInputs.test.js
+++ b/ui/hooks/gasFeeInput/useGasFeeInputs.test.js
@@ -4,6 +4,7 @@ import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transactio
import {
GAS_RECOMMENDATIONS,
CUSTOM_GAS_ESTIMATE,
+ EDIT_GAS_MODES,
} from '../../../shared/constants/gas';
import { ETH, PRIMARY } from '../../helpers/constants/common';
@@ -350,4 +351,13 @@ describe('useGasFeeInputs', () => {
expect(result.current.supportsEIP1559V2).toBe(false);
});
});
+
+ describe('editGasMode', () => {
+ it('should return editGasMode passed', () => {
+ const { result } = renderHook(() =>
+ useGasFeeInputs(undefined, undefined, undefined, EDIT_GAS_MODES.SWAPS),
+ );
+ expect(result.current.editGasMode).toBe(EDIT_GAS_MODES.SWAPS);
+ });
+ });
});
diff --git a/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js b/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
index 452c0611e..31c197c72 100644
--- a/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
+++ b/ui/hooks/gasFeeInput/useMaxFeePerGasInput.js
@@ -34,12 +34,12 @@ const getMaxFeePerGasFromTransaction = (transaction) => {
* method to update the setMaxFeePerGas.
*/
export function useMaxFeePerGasInput({
- EIP_1559_V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
+ supportsEIP1559V2,
transaction,
}) {
const supportsEIP1559 =
@@ -53,7 +53,7 @@ export function useMaxFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
- const maxFeePerGasFromTransaction = supportsEIP1559
+ const initialMaxFeePerGas = supportsEIP1559
? getMaxFeePerGasFromTransaction(transaction)
: 0;
@@ -61,16 +61,16 @@ export function useMaxFeePerGasInput({
// 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;
+ if (initialMaxFeePerGas && feeParamsAreCustom(transaction))
+ return initialMaxFeePerGas;
return null;
});
useEffect(() => {
- if (EIP_1559_V2) {
- setMaxFeePerGas(maxFeePerGasFromTransaction);
+ if (supportsEIP1559V2 && initialMaxFeePerGas) {
+ setMaxFeePerGas(initialMaxFeePerGas);
}
- }, [EIP_1559_V2, maxFeePerGasFromTransaction, setMaxFeePerGas]);
+ }, [initialMaxFeePerGas, setMaxFeePerGas, supportsEIP1559V2]);
let gasSettings = {
gasLimit: decimalToHex(gasLimit),
@@ -117,7 +117,7 @@ export function useMaxFeePerGasInput({
gasFeeEstimates,
gasEstimateType,
estimateToUse,
- maxFeePerGasFromTransaction,
+ initialMaxFeePerGas || 0,
);
return {
diff --git a/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js b/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
index 54aeb1093..7e85d7a3f 100644
--- a/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
+++ b/ui/hooks/gasFeeInput/useMaxPriorityFeePerGasInput.js
@@ -9,8 +9,8 @@ import {
checkNetworkAndAccountSupports1559,
getShouldShowFiat,
} from '../../selectors';
-import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
+import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
@@ -34,11 +34,11 @@ const getMaxPriorityFeePerGasFromTransaction = (transaction) => {
* method to update the maxPriorityFeePerGas.
*/
export function useMaxPriorityFeePerGasInput({
- EIP_1559_V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
+ supportsEIP1559V2,
transaction,
}) {
const supportsEIP1559 =
@@ -52,25 +52,21 @@ export function useMaxPriorityFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
- const maxPriorityFeePerGasFromTransaction = supportsEIP1559
+ const initialMaxPriorityFeePerGas = supportsEIP1559
? getMaxPriorityFeePerGasFromTransaction(transaction)
: 0;
const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(() => {
- if (maxPriorityFeePerGasFromTransaction && feeParamsAreCustom(transaction))
- return maxPriorityFeePerGasFromTransaction;
+ if (initialMaxPriorityFeePerGas && feeParamsAreCustom(transaction))
+ return initialMaxPriorityFeePerGas;
return null;
});
useEffect(() => {
- if (EIP_1559_V2) {
- setMaxPriorityFeePerGas(maxPriorityFeePerGasFromTransaction);
+ if (supportsEIP1559V2 && initialMaxPriorityFeePerGas) {
+ setMaxPriorityFeePerGas(initialMaxPriorityFeePerGas);
}
- }, [
- EIP_1559_V2,
- maxPriorityFeePerGasFromTransaction,
- setMaxPriorityFeePerGas,
- ]);
+ }, [initialMaxPriorityFeePerGas, setMaxPriorityFeePerGas, supportsEIP1559V2]);
const maxPriorityFeePerGasToUse =
maxPriorityFeePerGas ??
@@ -79,7 +75,7 @@ export function useMaxPriorityFeePerGasInput({
gasFeeEstimates,
gasEstimateType,
estimateToUse,
- maxPriorityFeePerGasFromTransaction,
+ initialMaxPriorityFeePerGas || 0,
);
// We need to display the estimated fiat currency impact of the
diff --git a/ui/hooks/gasFeeInput/useTransactionFunctions.js b/ui/hooks/gasFeeInput/useTransactionFunctions.js
index ab3830266..d3e0f0784 100644
--- a/ui/hooks/gasFeeInput/useTransactionFunctions.js
+++ b/ui/hooks/gasFeeInput/useTransactionFunctions.js
@@ -1,23 +1,33 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
-import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
+import { EDIT_GAS_MODES, PRIORITY_LEVELS } from '../../../shared/constants/gas';
import {
decimalToHex,
decGWEIToHexWEI,
} from '../../helpers/utils/conversions.util';
-import { updateTransaction as updateTransactionFn } from '../../store/actions';
+import {
+ updateCustomSwapsEIP1559GasParams,
+ updateSwapsUserFeeLevel,
+ updateTransaction as updateTransactionFn,
+} from '../../store/actions';
export const useTransactionFunctions = ({
defaultEstimateToUse,
+ editGasMode,
gasFeeEstimates,
- gasLimit,
+ gasLimit: gasLimitInTransaction,
transaction,
}) => {
const dispatch = useDispatch();
const updateTransaction = useCallback(
- (estimateUsed, maxFeePerGas, maxPriorityFeePerGas) => {
+ ({
+ estimateUsed,
+ maxFeePerGas,
+ maxPriorityFeePerGas,
+ gasLimit = gasLimitInTransaction,
+ }) => {
const newGasSettings = {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
@@ -33,16 +43,29 @@ export const useTransactionFunctions = ({
const updatedTxMeta = {
...transaction,
- userFeeLevel: estimateUsed || 'custom',
+ userFeeLevel: estimateUsed || PRIORITY_LEVELS.CUSTOM,
txParams: {
...transaction.txParams,
...newGasSettings,
},
};
- dispatch(updateTransactionFn(updatedTxMeta));
+ if (editGasMode === EDIT_GAS_MODES.SWAPS) {
+ dispatch(
+ updateSwapsUserFeeLevel(estimateUsed || PRIORITY_LEVELS.CUSTOM),
+ );
+ dispatch(updateCustomSwapsEIP1559GasParams(newGasSettings));
+ } else {
+ dispatch(updateTransactionFn(updatedTxMeta));
+ }
},
- [defaultEstimateToUse, dispatch, gasLimit, transaction],
+ [
+ defaultEstimateToUse,
+ dispatch,
+ editGasMode,
+ gasLimitInTransaction,
+ transaction,
+ ],
);
const updateTransactionUsingGasFeeEstimates = useCallback(
@@ -52,21 +75,21 @@ export const useTransactionFunctions = ({
maxFeePerGas,
maxPriorityFeePerGas,
} = transaction?.dappSuggestedGasFees;
- updateTransaction(
- PRIORITY_LEVELS.DAPP_SUGGESTED,
+ updateTransaction({
+ estimateUsed: PRIORITY_LEVELS.DAPP_SUGGESTED,
maxFeePerGas,
maxPriorityFeePerGas,
- );
+ });
} else {
const {
suggestedMaxFeePerGas,
suggestedMaxPriorityFeePerGas,
} = gasFeeEstimates[gasFeeEstimateToUse];
- updateTransaction(
- gasFeeEstimateToUse,
- decGWEIToHexWEI(suggestedMaxFeePerGas),
- decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
- );
+ updateTransaction({
+ estimateUsed: gasFeeEstimateToUse,
+ maxFeePerGas: decGWEIToHexWEI(suggestedMaxFeePerGas),
+ maxPriorityFeePerGas: decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
+ });
}
},
[gasFeeEstimates, transaction?.dappSuggestedGasFees, updateTransaction],
diff --git a/ui/hooks/useOriginMetadata.js b/ui/hooks/useOriginMetadata.js
index 31fb405d7..613bc88ba 100644
--- a/ui/hooks/useOriginMetadata.js
+++ b/ui/hooks/useOriginMetadata.js
@@ -1,14 +1,12 @@
import { useSelector } from 'react-redux';
-import { getDomainMetadata } from '../selectors';
+import { getSubjectMetadata } from '../selectors';
+import { SUBJECT_TYPES } from '../../shared/constants/app';
/**
* @typedef {Object} OriginMetadata
- * @property {string} host - The host of the origin
* @property {string} hostname - The hostname of the origin (host + port)
* @property {string} origin - The original origin string itself
- * @property {string} [icon] - The origin's site icon if available
- * @property {number} [lastUpdated] - Timestamp of the last update to the
- * origin's metadata
+ * @property {string} [iconUrl] - The origin's site icon URL, if available
* @property {string} [name] - The registered name of the origin if available
*/
@@ -20,22 +18,23 @@ import { getDomainMetadata } from '../selectors';
* current origin
*/
export function useOriginMetadata(origin) {
- const domainMetaData = useSelector(getDomainMetadata);
+ const subjectMetadata = useSelector(getSubjectMetadata);
if (!origin) {
return null;
}
- const url = new URL(origin);
+ const url = new URL(origin);
const minimumOriginMetadata = {
host: url.host,
hostname: url.hostname,
origin,
+ subjectType: SUBJECT_TYPES.UNKNOWN,
};
- if (domainMetaData?.[origin]) {
+ if (subjectMetadata?.[origin]) {
return {
...minimumOriginMetadata,
- ...domainMetaData[origin],
+ ...subjectMetadata[origin],
};
}
return minimumOriginMetadata;
diff --git a/ui/hooks/useTokenData.test.js b/ui/hooks/useTokenData.test.js
index 1e80f5c9e..ada5ec287 100644
--- a/ui/hooks/useTokenData.test.js
+++ b/ui/hooks/useTokenData.test.js
@@ -45,22 +45,22 @@ const tests = [
];
describe('useTokenData', () => {
- tests.forEach((test) => {
+ tests.forEach(({ data, tokenData }) => {
const testTitle =
// eslint-disable-next-line no-negated-condition
- test.tokenData !== null
- ? `should return properly decoded data with _value ${test.tokenData.args[1]}`
+ tokenData !== null
+ ? `should return properly decoded data with _value ${tokenData.args[1]}`
: `should return null when no data provided`;
it(`${testTitle}`, () => {
- const { result } = renderHook(() => useTokenData(test.data));
- if (test.tokenData) {
- expect(result.current.name).toStrictEqual(test.tokenData.name);
+ const { result } = renderHook(() => useTokenData(data));
+ if (tokenData) {
+ expect(result.current.name).toStrictEqual(tokenData.name);
expect(result.current.args[0].toLowerCase()).toStrictEqual(
- test.tokenData.args[0],
+ tokenData.args[0],
);
- expect(test.tokenData.args[1]).toStrictEqual(result.current.args[1]);
+ expect(tokenData.args[1]).toStrictEqual(result.current.args[1]);
} else {
- expect(result.current).toStrictEqual(test.tokenData);
+ expect(result.current).toStrictEqual(tokenData);
}
});
});
diff --git a/ui/hooks/useTokenDisplayValue.test.js b/ui/hooks/useTokenDisplayValue.test.js
index 91dc6df0b..1f4ccdb0c 100644
--- a/ui/hooks/useTokenDisplayValue.test.js
+++ b/ui/hooks/useTokenDisplayValue.test.js
@@ -118,20 +118,20 @@ const tests = [
];
describe('useTokenDisplayValue', () => {
- tests.forEach((test, idx) => {
- describe(`when input is decimals: ${test.token.decimals} and value: ${test.tokenValue}`, () => {
- it(`should return ${test.displayValue} as displayValue`, () => {
+ tests.forEach(({ displayValue, token, tokenData, tokenValue }, idx) => {
+ describe(`when input is decimals: ${token.decimals} and value: ${tokenValue}`, () => {
+ it(`should return ${displayValue} as displayValue`, () => {
const getTokenValueStub = sinon.stub(tokenUtil, 'getTokenValueParam');
const getTokenDataStub = sinon.stub(txUtil, 'getTokenData');
- getTokenDataStub.callsFake(() => test.tokenData);
- getTokenValueStub.callsFake(() => test.tokenValue);
+ getTokenDataStub.callsFake(() => tokenData);
+ getTokenValueStub.callsFake(() => tokenValue);
const { result } = renderHook(() =>
- useTokenDisplayValue(`${idx}-fakestring`, test.token),
+ useTokenDisplayValue(`${idx}-fakestring`, token),
);
sinon.restore();
- expect(result.current).toStrictEqual(test.displayValue);
+ expect(result.current).toStrictEqual(displayValue);
});
});
});
diff --git a/ui/hooks/useUserPreferencedCurrency.js b/ui/hooks/useUserPreferencedCurrency.js
index a4716e43f..205723218 100644
--- a/ui/hooks/useUserPreferencedCurrency.js
+++ b/ui/hooks/useUserPreferencedCurrency.js
@@ -1,4 +1,4 @@
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
import {
getPreferences,
getShouldShowFiat,
@@ -37,7 +37,10 @@ import { PRIMARY, SECONDARY, ETH } from '../helpers/constants/common';
*/
export function useUserPreferencedCurrency(type, opts = {}) {
const nativeCurrency = useSelector(getNativeCurrency);
- const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences);
+ const { useNativeCurrencyAsPrimaryCurrency } = useSelector(
+ getPreferences,
+ shallowEqual,
+ );
const showFiat = useSelector(getShouldShowFiat);
const currentCurrency = useSelector(getCurrentCurrency);
diff --git a/ui/pages/add-collectible/add-collectible.component.js b/ui/pages/add-collectible/add-collectible.js
similarity index 80%
rename from ui/pages/add-collectible/add-collectible.component.js
rename to ui/pages/add-collectible/add-collectible.js
index a0fc8639f..71e4002d7 100644
--- a/ui/pages/add-collectible/add-collectible.component.js
+++ b/ui/pages/add-collectible/add-collectible.js
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
+import { util } from '@metamask/controllers';
import { useI18nContext } from '../../hooks/useI18nContext';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
@@ -19,6 +20,7 @@ export default function AddCollectible() {
const [address, setAddress] = useState('');
const [tokenId, setTokenId] = useState('');
+ const [disabled, setDisabled] = useState(true);
const handleAddCollectible = async () => {
try {
@@ -33,6 +35,16 @@ export default function AddCollectible() {
history.push(DEFAULT_ROUTE);
};
+ const validateAndSetAddress = (val) => {
+ setDisabled(!util.isValidHexAddress(val) || !tokenId);
+ setAddress(val);
+ };
+
+ const validateAndSetTokenId = (val) => {
+ setDisabled(!util.isValidHexAddress(address) || !val);
+ setTokenId(val);
+ };
+
return (
{
history.push(DEFAULT_ROUTE);
}}
- disabled={false}
+ disabled={disabled}
contentComponent={
@@ -56,7 +68,7 @@ export default function AddCollectible() {
placeholder="0x..."
type="text"
value={address}
- onChange={(e) => setAddress(e.target.value)}
+ onChange={(e) => validateAndSetAddress(e.target.value)}
fullWidth
autoFocus
margin="normal"
@@ -69,7 +81,7 @@ export default function AddCollectible() {
placeholder={t('nftTokenIdPlaceholder')}
type="number"
value={tokenId}
- onChange={(e) => setTokenId(e.target.value)}
+ onChange={(e) => validateAndSetTokenId(e.target.value)}
fullWidth
margin="normal"
/>
diff --git a/ui/pages/add-collectible/index.js b/ui/pages/add-collectible/index.js
index 71b5d8dd5..43c35006c 100644
--- a/ui/pages/add-collectible/index.js
+++ b/ui/pages/add-collectible/index.js
@@ -1 +1 @@
-export { default } from './add-collectible.component';
+export { default } from './add-collectible';
diff --git a/ui/pages/asset/asset.js b/ui/pages/asset/asset.js
index 303d6fdeb..f48fd9bc4 100644
--- a/ui/pages/asset/asset.js
+++ b/ui/pages/asset/asset.js
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Redirect, useParams } from 'react-router-dom';
-import { getTokens } from '../../ducks/metamask/metamask';
+import CollectibleDetails from '../../components/app/collectible-details/collectible-details';
+import { getCollectibles, getTokens } from '../../ducks/metamask/metamask';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
@@ -11,19 +12,27 @@ import TokenAsset from './components/token-asset';
const Asset = () => {
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency);
const tokens = useSelector(getTokens);
- const { asset } = useParams();
+ const collectibles = useSelector(getCollectibles);
+ const { asset, id } = useParams();
const token = tokens.find(({ address }) =>
isEqualCaseInsensitive(address, asset),
);
+ const collectible = collectibles.find(
+ ({ address, tokenId }) =>
+ isEqualCaseInsensitive(address, asset) && id === tokenId,
+ );
+
useEffect(() => {
const el = document.querySelector('.app');
el.scroll(0, 0);
}, []);
let content;
- if (token) {
+ if (collectible) {
+ content = ;
+ } else if (token) {
content = ;
} else if (asset === nativeCurrency) {
content = ;
diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js
index bad912176..4969ef6f2 100644
--- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js
+++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js
@@ -7,7 +7,7 @@ import { updateMetamaskState } from '../../store/actions';
import ConfirmAddSuggestedToken from '.';
export default {
- title: 'Confirmation Screens',
+ title: 'Pages/ConfirmAddSuggestedToken',
id: __filename,
};
@@ -42,7 +42,7 @@ const PageSet = ({ children }) => {
return children;
};
-export const AddSuggestedToken = () => {
+export const DefaultStory = () => {
const state = store.getState();
store.dispatch(
updateMetamaskState(
@@ -58,3 +58,5 @@ export const AddSuggestedToken = () => {
);
};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
index 47475b5a4..3a7f080a6 100644
--- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
+++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
@@ -11,6 +11,7 @@ import { ellipsify } from '../../send/send.utils';
import Typography from '../../../components/ui/typography';
import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button';
+import EditGasFeeButton from '../../../components/app/edit-gas-fee-button';
import MetaFoxLogo from '../../../components/ui/metafox-logo';
import Identicon from '../../../components/ui/identicon';
import MultiLayerFeeMessage from '../../../components/app/multilayer-fee-message';
@@ -64,6 +65,7 @@ export default class ConfirmApproveContent extends Component {
isContract: PropTypes.bool,
hexTransactionTotal: PropTypes.string,
isMultiLayerFeeNetwork: PropTypes.bool,
+ supportsEIP1559V2: PropTypes.bool,
};
state = {
@@ -76,11 +78,13 @@ export default class ConfirmApproveContent extends Component {
symbol,
title,
showEdit,
+ showAdvanceGasFeeOptions = false,
onEditClick,
content,
footer,
noBorder,
}) {
+ const { supportsEIP1559V2 } = this.props;
const { t } = this.context;
return (
{title}
- {showEdit && (
+ {showEdit && (!showAdvanceGasFeeOptions || !supportsEIP1559V2) && (
)}
+ {showEdit && showAdvanceGasFeeOptions && supportsEIP1559V2 && (
+
+ )}
@@ -445,6 +452,7 @@ export default class ConfirmApproveContent extends Component {
symbol:
,
title: t('transactionFee'),
showEdit: true,
+ showAdvanceGasFeeOptions: true,
onEditClick: showCustomizeGasModal,
content: this.renderTransactionDetailsContent(),
noBorder: useNonceField || !showFullTxDetails,
diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js
index 4d4aa1614..2cf223bf9 100644
--- a/ui/pages/confirm-approve/confirm-approve.js
+++ b/ui/pages/confirm-approve/confirm-approve.js
@@ -15,6 +15,8 @@ import {
getTokenValueParam,
} from '../../helpers/utils/token-util';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
+import { GasFeeContextProvider } from '../../contexts/gasFee';
+import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
import { useTokenTracker } from '../../hooks/useTokenTracker';
import {
getTokens,
@@ -25,20 +27,21 @@ import {
transactionFeeSelector,
txDataSelector,
getCurrentCurrency,
- getDomainMetadata,
+ getSubjectMetadata,
getUseNonceField,
getCustomNonceValue,
getNextSuggestedNonce,
getCurrentChainId,
getRpcPrefsForCurrentProvider,
getIsMultiLayerFeeNetwork,
+ checkNetworkAndAccountSupports1559,
} from '../../selectors';
-
import { useApproveTransaction } from '../../hooks/useApproveTransaction';
-
import { currentNetworkTxListSelector } from '../../selectors/transactions';
-import Loading from '../../components/ui/loading-screen';
+import AdvancedGasFeePopover from '../../components/app/advanced-gas-fee-popover';
+import EditGasFeePopover from '../../components/app/edit-gas-fee-popover';
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component';
+import Loading from '../../components/ui/loading-screen';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
import { getCustomTxParamsData } from './confirm-approve.util';
import ConfirmApproveContent from './confirm-approve-content';
@@ -47,6 +50,10 @@ const isAddressLedgerByFromAddress = (address) => (state) => {
return isAddressLedger(state, address);
};
+// eslint-disable-next-line prefer-destructuring
+const EIP_1559_V2_ENABLED =
+ process.env.EIP_1559_V2 === true || process.env.EIP_1559_V2 === 'true';
+
export default function ConfirmApprove() {
const dispatch = useDispatch();
const { id: paramsTransactionId } = useParams();
@@ -58,7 +65,7 @@ export default function ConfirmApprove() {
const currentCurrency = useSelector(getCurrentCurrency);
const nativeCurrency = useSelector(getNativeCurrency);
const currentNetworkTxList = useSelector(currentNetworkTxListSelector);
- const domainMetadata = useSelector(getDomainMetadata);
+ const subjectMetadata = useSelector(getSubjectMetadata);
const tokens = useSelector(getTokens);
const useNonceField = useSelector(getUseNonceField);
const nextNonce = useSelector(getNextSuggestedNonce);
@@ -66,6 +73,9 @@ export default function ConfirmApprove() {
const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const isMultiLayerFeeNetwork = useSelector(getIsMultiLayerFeeNetwork);
+ const networkAndAccountSupports1559 = useSelector(
+ checkNetworkAndAccountSupports1559,
+ );
const fromAddressIsLedger = useSelector(isAddressLedgerByFromAddress(from));
@@ -79,6 +89,9 @@ export default function ConfirmApprove() {
hexTransactionTotal,
} = useSelector((state) => transactionFeeSelector(state, transaction));
+ const supportsEIP1559V2 =
+ EIP_1559_V2_ENABLED && networkAndAccountSupports1559;
+
const currentToken = (tokens &&
tokens.find(({ address }) =>
isEqualCaseInsensitive(tokenAddress, address),
@@ -150,7 +163,7 @@ export default function ConfirmApprove() {
const { origin } = transaction;
const formattedOrigin = origin || '';
- const { icon: siteImage = '' } = domainMetadata[origin] || {};
+ const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {};
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`;
const tokenBalance = tokenTrackerBalance
@@ -163,101 +176,110 @@ export default function ConfirmApprove() {
return tokenSymbol === undefined ? (