diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c33171c0e..bc842087e 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Προσθήκη σημειώματος" }, - "addNFT": { - "message": "Προσθήκη NFT" - }, "addNetwork": { "message": "Προσθήκη Δικτύου" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 206aababa..b70a85694 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Add memo" }, - "addNFT": { - "message": "Add NFT" - }, "addNetwork": { "message": "Add Network" }, @@ -458,6 +455,10 @@ "close": { "message": "Close" }, + "collectibleAddressError": { + "message": "This token is an NFT. Add on the $1", + "description": "$1 is a clickable link with text defined by the 'importNFTPage' key" + }, "confirm": { "message": "Confirm" }, @@ -1402,6 +1403,12 @@ "importMyWallet": { "message": "Import My Wallet" }, + "importNFT": { + "message": "Import NFT" + }, + "importNFTPage": { + "message": "Import NFT page" + }, "importNFTs": { "message": "Import NFTs" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index a01a75db8..30a055b87 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Ajouter un mémo" }, - "addNFT": { - "message": "Ajouter un NFT" - }, "addNetwork": { "message": "Ajouter un réseau" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 580fbdd35..b6cf3ed1a 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "मेमो जोड़ें" }, - "addNFT": { - "message": "NFT जोड़ें" - }, "addNetwork": { "message": "नेटवर्क जोड़ें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 18ac64d24..32a1485df 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Tambahkan memo" }, - "addNFT": { - "message": "Tambahkan NFT" - }, "addNetwork": { "message": "Tambahkan Jaringan" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 381b4c943..c6d4be056 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "メモを追加" }, - "addNFT": { - "message": "NFTを追加" - }, "addNetwork": { "message": "ネットワークの追加" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d2ab73621..1645eff00 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "메모 추가" }, - "addNFT": { - "message": "NFT 추가" - }, "addNetwork": { "message": "네트워크 추가" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index e438e24b0..2623ae63d 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Добавить примечание" }, - "addNFT": { - "message": "Добавить NFT" - }, "addNetwork": { "message": "Добавить сеть" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 5f9eb4d03..f90ef7fe6 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Magdagdag ng memo" }, - "addNFT": { - "message": "Magdagdag ng NFT" - }, "addNetwork": { "message": "Magdagdag ng Network" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 2fc5cba1c..bab183e51 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Not ekleyin" }, - "addNFT": { - "message": "NFT ekleyin" - }, "addNetwork": { "message": "Ağ ekleyin" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 060c5e485..7f2ad991a 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "Thêm bản ghi nhớ" }, - "addNFT": { - "message": "Thêm NFT" - }, "addNetwork": { "message": "Thêm mạng" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index d1fb4e637..b90d3be9c 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -140,9 +140,6 @@ "addMemo": { "message": "添加备忘录" }, - "addNFT": { - "message": "添加NFT" - }, "addNetwork": { "message": "添加网络" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 205472581..bcf85dd52 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -220,22 +220,22 @@ export default class MetamaskController extends EventEmitter { onNetworkStateChange: this.networkController.store.subscribe.bind( this.networkController.store, ), - getAssetName: this.assetsContractController.getAssetName.bind( + getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( this.assetsContractController, ), - getAssetSymbol: this.assetsContractController.getAssetSymbol.bind( + getERC721AssetSymbol: this.assetsContractController.getERC721AssetSymbol.bind( this.assetsContractController, ), - getCollectibleTokenURI: this.assetsContractController.getCollectibleTokenURI.bind( + getERC721TokenURI: this.assetsContractController.getERC721TokenURI.bind( this.assetsContractController, ), - getOwnerOf: this.assetsContractController.getOwnerOf.bind( + getERC721OwnerOf: this.assetsContractController.getERC721OwnerOf.bind( this.assetsContractController, ), - balanceOfERC1155Collectible: this.assetsContractController.balanceOfERC1155Collectible.bind( + getERC1155BalanceOf: this.assetsContractController.getERC1155BalanceOf.bind( this.assetsContractController, ), - uriERC1155Collectible: this.assetsContractController.uriERC1155Collectible.bind( + getERC1155TokenURI: this.assetsContractController.getERC1155TokenURI.bind( this.assetsContractController, ), }, @@ -1040,6 +1040,7 @@ export default class MetamaskController extends EventEmitter { appStateController, collectiblesController, collectibleDetectionController, + assetsContractController, currencyRateController, detectTokensController, ensController, @@ -1192,6 +1193,11 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), + // AssetsContractController + getTokenStandardAndDetails: assetsContractController.getTokenStandardAndDetails.bind( + assetsContractController, + ), + // CollectiblesController addCollectible: collectiblesController.addCollectible.bind( collectiblesController, diff --git a/package.json b/package.json index e9531d171..acaf95868 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@keystonehq/metamask-airgapped-keyring": "0.2.1", "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.31.0", - "@metamask/controllers": "^24.0.0", + "@metamask/controllers": "^25.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", diff --git a/ui/components/ui/page-container/index.scss b/ui/components/ui/page-container/index.scss index a249bb912..45f5f8854 100644 --- a/ui/components/ui/page-container/index.scss +++ b/ui/components/ui/page-container/index.scss @@ -28,6 +28,7 @@ right: 16px; cursor: pointer; overflow: hidden; + background-color: transparent; &::after { content: '\00D7'; diff --git a/ui/components/ui/page-container/page-container-header/page-container-header.component.js b/ui/components/ui/page-container/page-container-header/page-container-header.component.js index 78f71b65a..9b4787e0e 100644 --- a/ui/components/ui/page-container/page-container-header/page-container-header.component.js +++ b/ui/components/ui/page-container/page-container-header/page-container-header.component.js @@ -47,9 +47,10 @@ export default class PageContainerHeader extends Component { return ( onClose && ( -
onClose()} + aria-label="close" /> ) ); diff --git a/ui/components/ui/text-field/text-field.component.js b/ui/components/ui/text-field/text-field.component.js index 8720ff19b..44a96ee3f 100644 --- a/ui/components/ui/text-field/text-field.component.js +++ b/ui/components/ui/text-field/text-field.component.js @@ -246,7 +246,7 @@ TextField.propTypes = { /** * Show error message */ - error: PropTypes.string, + error: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), /** * Add custom CSS class */ diff --git a/ui/pages/add-collectible/add-collectible.js b/ui/pages/add-collectible/add-collectible.js index 71e4002d7..2587d1073 100644 --- a/ui/pages/add-collectible/add-collectible.js +++ b/ui/pages/add-collectible/add-collectible.js @@ -17,8 +17,13 @@ export default function AddCollectible() { const t = useI18nContext(); const history = useHistory(); const dispatch = useDispatch(); + const addressEnteredOnImportTokensPage = + history?.location?.state?.addressEnteredOnImportTokensPage; + + const [address, setAddress] = useState( + addressEnteredOnImportTokensPage ?? '', + ); - const [address, setAddress] = useState(''); const [tokenId, setTokenId] = useState(''); const [disabled, setDisabled] = useState(true); @@ -47,7 +52,7 @@ export default function AddCollectible() { return ( { handleAddCollectible(); }} diff --git a/ui/pages/import-token/import-token.component.js b/ui/pages/import-token/import-token.component.js index dc2dbf1fe..8faa500a0 100644 --- a/ui/pages/import-token/import-token.component.js +++ b/ui/pages/import-token/import-token.component.js @@ -8,6 +8,7 @@ import { } from '../../helpers/utils/util'; import { tokenInfoGetter } from '../../helpers/utils/token-util'; import { + ADD_COLLECTIBLE_ROUTE, CONFIRM_IMPORT_TOKEN_ROUTE, EXPERIMENTAL_ROUTE, } from '../../helpers/constants/routes'; @@ -46,6 +47,8 @@ class ImportToken extends Component { rpcPrefs: PropTypes.object, tokenList: PropTypes.object, useTokenDetection: PropTypes.bool, + getTokenStandardAndDetails: PropTypes.func, + selectedAddress: PropTypes.string, }; static defaultProps = { @@ -62,6 +65,7 @@ class ImportToken extends Component { customAddressError: null, customSymbolError: null, customDecimalsError: null, + collectibleAddressError: null, forceEditSymbol: false, symbolAutoFilled: false, decimalAutoFilled: false, @@ -126,13 +130,15 @@ class ImportToken extends Component { customAddressError, customSymbolError, customDecimalsError, + collectibleAddressError, } = this.state; return ( tokenSelectorError || customAddressError || customSymbolError || - customDecimalsError + customDecimalsError || + collectibleAddressError ); } @@ -186,11 +192,12 @@ class ImportToken extends Component { this.handleCustomDecimalsChange(decimals); } - handleCustomAddressChange(value) { + async handleCustomAddressChange(value) { const customAddress = value.trim(); this.setState({ customAddress, customAddressError: null, + collectibleAddressError: null, tokenSelectorError: null, symbolAutoFilled: false, decimalAutoFilled: false, @@ -208,8 +215,23 @@ class ImportToken extends Component { const isMainnetNetwork = this.props.chainId === '0x1'; + let standard; + if (addressIsValid) { + try { + ({ standard } = await this.props.getTokenStandardAndDetails( + standardAddress, + this.props.selectedAddress, + )); + } catch (error) { + // ignore + } + } + + const addressIsEmpty = + customAddress.length === 0 || customAddress === emptyAddr; + switch (true) { - case !addressIsValid: + case !addressIsValid && !addressIsEmpty: this.setState({ customAddressError: this.context.t('invalidAddress'), customSymbol: '', @@ -218,6 +240,28 @@ class ImportToken extends Component { customDecimalsError: null, }); + break; + case process.env.COLLECTIBLES_V1 && + (standard === 'ERC1155' || standard === 'ERC721'): + this.setState({ + collectibleAddressError: this.context.t('collectibleAddressError', [ + + this.props.history.push({ + pathname: ADD_COLLECTIBLE_ROUTE, + state: { + addressEnteredOnImportTokensPage: this.state.customAddress, + }, + }) + } + key="collectibleAddressError" + > + {this.context.t('importNFTPage')} + , + ]), + }); + break; case isMainnetToken && !isMainnetNetwork: this.setState({ @@ -242,7 +286,7 @@ class ImportToken extends Component { break; default: - if (customAddress !== emptyAddr) { + if (!addressIsEmpty) { this.attemptToAutoFillTokenParams(customAddress); } } @@ -290,6 +334,7 @@ class ImportToken extends Component { symbolAutoFilled, decimalAutoFilled, mainnetTokenWarning, + collectibleAddressError, } = this.state; const { chainId, rpcPrefs } = this.props; @@ -330,7 +375,9 @@ class ImportToken extends Component { type="text" value={customAddress} onChange={(e) => this.handleCustomAddressChange(e.target.value)} - error={customAddressError || mainnetTokenWarning} + error={ + customAddressError || mainnetTokenWarning || collectibleAddressError + } fullWidth autoFocus margin="normal" diff --git a/ui/pages/import-token/import-token.container.js b/ui/pages/import-token/import-token.container.js index f6fa265f1..ae3d97228 100644 --- a/ui/pages/import-token/import-token.container.js +++ b/ui/pages/import-token/import-token.container.js @@ -1,6 +1,10 @@ import { connect } from 'react-redux'; -import { setPendingTokens, clearPendingTokens } from '../../store/actions'; +import { + setPendingTokens, + clearPendingTokens, + getTokenStandardAndDetails, +} from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getRpcPrefsForCurrentProvider, @@ -17,6 +21,7 @@ const mapStateToProps = (state) => { provider: { chainId }, useTokenDetection, tokenList, + selectedAddress, }, } = state; const showSearchTabCustomNetwork = @@ -33,13 +38,15 @@ const mapStateToProps = (state) => { rpcPrefs: getRpcPrefsForCurrentProvider(state), tokenList, useTokenDetection, + selectedAddress, }; }; - const mapDispatchToProps = (dispatch) => { return { setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)), clearPendingTokens: () => dispatch(clearPendingTokens()), + getTokenStandardAndDetails: (address, selectedAddress) => + getTokenStandardAndDetails(address, selectedAddress, null), }; }; diff --git a/ui/pages/import-token/import-token.test.js b/ui/pages/import-token/import-token.test.js index c6be1f1bb..c03a9c486 100644 --- a/ui/pages/import-token/import-token.test.js +++ b/ui/pages/import-token/import-token.test.js @@ -1,110 +1,177 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import sinon from 'sinon'; -import configureMockStore from 'redux-mock-store'; -import { mountWithRouter } from '../../../test/lib/render-helpers'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../test/lib/render-helpers'; +import configureStore from '../../store/store'; +import { + setPendingTokens, + clearPendingTokens, + getTokenStandardAndDetails, +} from '../../store/actions'; import ImportToken from './import-token.container'; +jest.mock('../../store/actions', () => ({ + getTokenStandardAndDetails: jest + .fn() + .mockImplementation(() => Promise.resolve({ standard: 'ERC20' })), + setPendingTokens: jest + .fn() + .mockImplementation(() => ({ type: 'SET_PENDING_TOKENS' })), + clearPendingTokens: jest + .fn() + .mockImplementation(() => ({ type: 'CLEAR_PENDING_TOKENS' })), +})); + describe('Import Token', () => { - let wrapper; - - const state = { - metamask: { - tokens: [], - }, - }; - - const store = configureMockStore()(state); - + const historyStub = jest.fn(); const props = { history: { - push: sinon.stub().callsFake(() => undefined), + push: historyStub, }, - setPendingTokens: sinon.spy(), - clearPendingTokens: sinon.spy(), - tokens: [], - identities: {}, - mostRecentOverviewPage: '/', showSearchTab: true, tokenList: {}, }; + const render = () => { + const baseStore = { + metamask: { + tokens: [], + provider: { chainId: '0x1' }, + frequentRpcListDetail: [], + identities: {}, + selectedAddress: '0x1231231', + }, + history: { + mostRecentOverviewPage: '/', + }, + }; + + const store = configureStore(baseStore); + + return renderWithProvider(, store); + }; + describe('Import Token', () => { - beforeAll(() => { - wrapper = mountWithRouter( - - - , - store, - ); + it('add Custom Token button is disabled when no fields are populated', () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); + const submit = getByText('Add Custom Token'); - wrapper.find({ name: 'customToken' }).simulate('click'); - }); - - afterEach(() => { - props.history.push.reset(); - }); - - it('next button is disabled when no fields are populated', () => { - const nextButton = wrapper.find( - '.button.btn-primary.page-container__footer-button', - ); - - expect(nextButton.props().disabled).toStrictEqual(true); + expect(submit).toBeDisabled(); }); it('edits token address', () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; const event = { target: { value: tokenAddress } }; - const customAddress = wrapper.find('input#custom-address'); + fireEvent.change(document.getElementById('custom-address'), event); - customAddress.simulate('change', event); - expect( - wrapper.find('ImportToken').instance().state.customAddress, - ).toStrictEqual(tokenAddress); + expect(document.getElementById('custom-address').value).toStrictEqual( + tokenAddress, + ); }); it('edits token symbol', () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); + const tokenSymbol = 'META'; const event = { target: { value: tokenSymbol } }; - const customAddress = wrapper.find('#custom-symbol'); - customAddress.last().simulate('change', event); + fireEvent.change(document.getElementById('custom-symbol'), event); - expect( - wrapper.find('ImportToken').instance().state.customSymbol, - ).toStrictEqual(tokenSymbol); + expect(document.getElementById('custom-symbol').value).toStrictEqual( + tokenSymbol, + ); }); it('edits token decimal precision', () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); + const tokenPrecision = '2'; const event = { target: { value: tokenPrecision } }; - const customAddress = wrapper.find('#custom-decimals'); - customAddress.last().simulate('change', event); + fireEvent.change(document.getElementById('custom-decimals'), event); - expect( - wrapper.find('ImportToken').instance().state.customDecimals, - ).toStrictEqual(Number(tokenPrecision)); - }); - - it('next', () => { - const nextButton = wrapper.find( - '.button.btn-primary.page-container__footer-button', - ); - nextButton.simulate('click'); - - expect(props.setPendingTokens.calledOnce).toStrictEqual(true); - expect(props.history.push.calledOnce).toStrictEqual(true); - expect(props.history.push.getCall(0).args[0]).toStrictEqual( - '/confirm-import-token', + expect(document.getElementById('custom-decimals').value).toStrictEqual( + tokenPrecision, ); }); - it('cancels', () => { - const cancelButton = wrapper.find('.page-container__header-close'); - cancelButton.simulate('click'); + it('adds custom tokens successfully', async () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); - expect(props.clearPendingTokens.calledOnce).toStrictEqual(true); - expect(props.history.push.getCall(0).args[0]).toStrictEqual('/'); + const submit = getByText('Add Custom Token'); + expect(submit).toBeDisabled(); + + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; + fireEvent.change(document.getElementById('custom-address'), { + target: { value: tokenAddress }, + }); + expect(submit).not.toBeDisabled(); + + const tokenSymbol = 'META'; + fireEvent.change(document.getElementById('custom-symbol'), { + target: { value: tokenSymbol }, + }); + + const tokenPrecision = '2'; + await fireEvent.change(document.getElementById('custom-decimals'), { + target: { value: tokenPrecision }, + }); + + expect(submit).not.toBeDisabled(); + fireEvent.click(submit); + expect(setPendingTokens).toHaveBeenCalledWith({ + customToken: { + address: tokenAddress, + decimals: Number(tokenPrecision), + symbol: tokenSymbol, + }, + selectedTokens: {}, + tokenAddressList: [], + }); + expect(historyStub).toHaveBeenCalledWith('/confirm-import-token'); + }); + + it('cancels out of import token flow', () => { + const { getByRole } = render(); + const closeButton = getByRole('button', { name: 'close' }); + fireEvent.click(closeButton); + + expect(clearPendingTokens).toHaveBeenCalled(); + expect(historyStub).toHaveBeenCalledWith('/'); + }); + + it('sets and error when a token is an NFT', async () => { + process.env.COLLECTIBLES_V1 = true; + getTokenStandardAndDetails.mockImplementation(() => + Promise.resolve({ standard: 'ERC721' }), + ); + + const { getByText } = render(); + const customTokenButton = getByText('Custom Token'); + fireEvent.click(customTokenButton); + + const submit = getByText('Add Custom Token'); + expect(submit).toBeDisabled(); + + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; + await fireEvent.change(document.getElementById('custom-address'), { + target: { value: tokenAddress }, + }); + + expect(submit).toBeDisabled(); + + // The last part of this error message won't be found by getByText because it is wrapped as a link. + const errorMessage = getByText('This token is an NFT. Add on the'); + expect(errorMessage).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/import-token/index.scss b/ui/pages/import-token/index.scss index b687b9170..178334fda 100644 --- a/ui/pages/import-token/index.scss +++ b/ui/pages/import-token/index.scss @@ -6,12 +6,12 @@ &__custom-token-form { padding: 8px 16px 16px; - input[type="number"]::-webkit-inner-spin-button { + input[type='number']::-webkit-inner-spin-button { -webkit-appearance: none; display: none; } - input[type="number"]:hover::-webkit-inner-spin-button { + input[type='number']:hover::-webkit-inner-spin-button { -webkit-appearance: none; display: none; } @@ -59,4 +59,13 @@ margin-bottom: 16px; margin-top: 0; } + + &__collectible-address-error-link { + color: var(--primary-blue); + cursor: pointer; + + &:hover { + color: var(--Blue-400); + } + } } diff --git a/ui/store/actions.js b/ui/store/actions.js index 480c34ee0..6473d2c58 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -1418,6 +1418,18 @@ export async function checkAndUpdateSingleCollectibleOwnershipStatus( ); } +export async function getTokenStandardAndDetails( + address, + userAddress, + tokenId, +) { + return await promisifiedBackground.getTokenStandardAndDetails( + address, + userAddress, + tokenId, + ); +} + export function removeToken(address) { return async (dispatch) => { dispatch(showLoadingIndication()); diff --git a/yarn.lock b/yarn.lock index 0fe4f3931..036efd66e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2652,10 +2652,10 @@ web3 "^0.20.7" web3-provider-engine "^16.0.3" -"@metamask/controllers@^24.0.0": - version "24.0.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-24.0.0.tgz#380d4e10bd9b903afc38b041ea7e64b30ae34dab" - integrity sha512-gOTpvxQTNTrXOUpGOiRKcqHUgnjSiTgJcwNLxenZWqPIixz7eI2qHnjO7ZaGbV/baK7SOa4rRGokvsCNV0mafA== +"@metamask/controllers@^25.0.0": + version "25.0.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-25.0.0.tgz#764ba46a169f197253e35ca5273720c91eae7662" + integrity sha512-BfTRaK87Iha+EXlEsu330Jp9Wtx0gks2vA+q3Lu/AKGbcz2J0cokECNwN7uEe4GQLPLsfCIAkUIAlP3pRiCvjg== dependencies: "@ethereumjs/common" "^2.3.1" "@ethereumjs/tx" "^3.2.1"